Add method to send notification bundle by SMS (#4624)
# What this PR does Adds method to render and send notification bundle by sms. Example of SMS message: ``` Grafana OnCall: Alert groups #1, #2, #3 and 2 more from stack: TestOrganization, integrations: Grafana Alerting and 1 more. ``` Should be merged with https://github.com/grafana/oncall/pull/4457 ## Which issue(s) this PR closes https://github.com/grafana/oncall-private/issues/2713 ## 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] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
This commit is contained in:
parent
f7e406ca6f
commit
35ddfab0e4
12 changed files with 410 additions and 64 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import typing
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from apps.alerts.models import Alert, AlertGroup
|
||||
from apps.alerts.models import Alert, AlertGroup, BundledNotification
|
||||
|
||||
|
||||
class AlertBaseRenderer(ABC):
|
||||
|
|
@ -33,3 +34,11 @@ class AlertGroupBaseRenderer(ABC):
|
|||
@abstractmethod
|
||||
def alert_renderer_class(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AlertGroupBundleBaseRenderer:
|
||||
MAX_ALERT_GROUPS_TO_RENDER = 3
|
||||
MAX_CHANNELS_TO_RENDER = 1
|
||||
|
||||
def __init__(self, notifications: "QuerySet[BundledNotification]"):
|
||||
self.notifications = notifications
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
from apps.alerts.incident_appearance.renderers.base_renderer import AlertBaseRenderer, AlertGroupBaseRenderer
|
||||
from django.db.models import Count
|
||||
|
||||
from apps.alerts.incident_appearance.renderers.base_renderer import (
|
||||
AlertBaseRenderer,
|
||||
AlertGroupBaseRenderer,
|
||||
AlertGroupBundleBaseRenderer,
|
||||
)
|
||||
from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_TITLE
|
||||
from apps.alerts.incident_appearance.templaters import AlertSmsTemplater
|
||||
from common.utils import str_or_backup
|
||||
|
|
@ -24,3 +30,53 @@ class AlertGroupSmsRenderer(AlertGroupBaseRenderer):
|
|||
f"integration: {self.alert_group.channel.short_name}, "
|
||||
f"alerts registered: {self.alert_group.alerts.count()}."
|
||||
)
|
||||
|
||||
|
||||
class AlertGroupSMSBundleRenderer(AlertGroupBundleBaseRenderer):
|
||||
def render(self) -> str:
|
||||
"""
|
||||
Renders SMS message for notification bundle: gets total count of unique alert groups and alert receive channels
|
||||
in the bundle, renders text with `inside_organization_number` of 3 alert groups (MAX_ALERT_GROUPS_TO_RENDER) and
|
||||
`short_name` of 1 alert receive channel (MAX_CHANNELS_TO_RENDER). If there are more unique alert groups / alert
|
||||
receive channels to notify about, adds "and X more" to the SMS message
|
||||
"""
|
||||
|
||||
channels_and_alert_groups_count = self.notifications.aggregate(
|
||||
channels_count=Count("alert_receive_channel", distinct=True),
|
||||
alert_groups_count=Count("alert_group", distinct=True),
|
||||
)
|
||||
alert_groups_count = channels_and_alert_groups_count["alert_groups_count"]
|
||||
channels_count = channels_and_alert_groups_count["channels_count"]
|
||||
|
||||
# get 3 unique alert groups from notifications
|
||||
alert_groups_to_render = []
|
||||
for notification in self.notifications:
|
||||
if notification.alert_group not in alert_groups_to_render:
|
||||
alert_groups_to_render.append(notification.alert_group)
|
||||
if len(alert_groups_to_render) == self.MAX_ALERT_GROUPS_TO_RENDER:
|
||||
break
|
||||
# render text for alert groups
|
||||
alert_group_inside_organization_numbers = [
|
||||
alert_group.inside_organization_number for alert_group in alert_groups_to_render
|
||||
]
|
||||
numbers_str = ", ".join(f"#{x}" for x in alert_group_inside_organization_numbers)
|
||||
alert_groups_text = "Alert groups " if alert_groups_count > 1 else "Alert group "
|
||||
alert_groups_text += numbers_str
|
||||
|
||||
if alert_groups_count > self.MAX_ALERT_GROUPS_TO_RENDER:
|
||||
alert_groups_text += f" and {alert_groups_count - self.MAX_ALERT_GROUPS_TO_RENDER} more"
|
||||
|
||||
# render text for alert receive channels
|
||||
channels_to_render = [alert_groups_to_render[i].channel for i in range(self.MAX_CHANNELS_TO_RENDER)]
|
||||
channel_names = ", ".join([channel.short_name for channel in channels_to_render])
|
||||
channels_text = "integrations: " if channels_count > 1 else "integration: "
|
||||
channels_text += channel_names
|
||||
|
||||
if channels_count > self.MAX_CHANNELS_TO_RENDER:
|
||||
channels_text += f" and {channels_count - self.MAX_CHANNELS_TO_RENDER} more"
|
||||
|
||||
return (
|
||||
f"Grafana OnCall: {alert_groups_text} "
|
||||
f"from stack: {channels_to_render[0].organization.stack_slug}, "
|
||||
f"{channels_text}."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import pytest
|
||||
|
||||
from apps.alerts.incident_appearance.renderers.sms_renderer import AlertGroupSMSBundleRenderer
|
||||
from apps.alerts.incident_appearance.templaters import AlertSlackTemplater, AlertWebTemplater
|
||||
from apps.alerts.models import AlertGroup
|
||||
from apps.base.models import UserNotificationPolicy
|
||||
from config_integrations import grafana
|
||||
|
||||
|
||||
|
|
@ -163,3 +165,58 @@ def test_get_resolved_text(
|
|||
alert_group.resolve(resolved_by=source, resolved_by_user=user)
|
||||
|
||||
assert alert_group.get_resolve_text() == expected_text.format(username=user.get_username_with_slack_verbal())
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_sms_bundle_renderer(
|
||||
make_organization_and_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_user_notification_bundle,
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
alert_receive_channel_1 = make_alert_receive_channel(
|
||||
organization,
|
||||
)
|
||||
alert_receive_channel_2 = make_alert_receive_channel(
|
||||
organization,
|
||||
)
|
||||
alert_group_1 = make_alert_group(alert_receive_channel_1)
|
||||
alert_group_2 = make_alert_group(alert_receive_channel_1)
|
||||
alert_group_3 = make_alert_group(alert_receive_channel_1)
|
||||
alert_group_4 = make_alert_group(alert_receive_channel_2)
|
||||
|
||||
notification_bundle = make_user_notification_bundle(user, UserNotificationPolicy.NotificationChannel.SMS)
|
||||
|
||||
# render 1 alert group and 1 channel
|
||||
notification_bundle.append_notification(alert_group_1, None)
|
||||
renderer = AlertGroupSMSBundleRenderer(notification_bundle.notifications.all())
|
||||
message = renderer.render()
|
||||
assert message == (
|
||||
f"Grafana OnCall: Alert group #{alert_group_1.inside_organization_number} "
|
||||
f"from stack: {organization.stack_slug}, "
|
||||
f"integration: {alert_receive_channel_1.short_name}."
|
||||
)
|
||||
|
||||
# render 3 alert groups and 1 channel
|
||||
notification_bundle.append_notification(alert_group_2, None)
|
||||
notification_bundle.append_notification(alert_group_3, None)
|
||||
renderer = AlertGroupSMSBundleRenderer(notification_bundle.notifications.all())
|
||||
message = renderer.render()
|
||||
assert message == (
|
||||
f"Grafana OnCall: Alert groups #{alert_group_1.inside_organization_number}, "
|
||||
f"#{alert_group_2.inside_organization_number}, #{alert_group_3.inside_organization_number} "
|
||||
f"from stack: {organization.stack_slug}, "
|
||||
f"integration: {alert_receive_channel_1.short_name}."
|
||||
)
|
||||
|
||||
# render 4 alert groups and 2 channels
|
||||
notification_bundle.append_notification(alert_group_4, None)
|
||||
renderer = AlertGroupSMSBundleRenderer(notification_bundle.notifications.all())
|
||||
message = renderer.render()
|
||||
assert message == (
|
||||
f"Grafana OnCall: Alert groups #{alert_group_1.inside_organization_number}, "
|
||||
f"#{alert_group_2.inside_organization_number}, #{alert_group_3.inside_organization_number} and 1 more "
|
||||
f"from stack: {organization.stack_slug}, "
|
||||
f"integrations: {alert_receive_channel_1.short_name} and 1 more."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def get_call_status_callback_url():
|
|||
|
||||
|
||||
def update_exotel_call_status(call_id: str, call_status: str, user_choice: Optional[str] = None):
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
|
||||
status_code = ExotelCallStatuses.DETERMINANT.get(call_status)
|
||||
if status_code is None:
|
||||
|
|
@ -62,12 +62,8 @@ def update_exotel_call_status(call_id: str, call_status: str, user_choice: Optio
|
|||
author=phone_call_record.receiver,
|
||||
notification_policy=phone_call_record.notification_policy,
|
||||
alert_group=phone_call_record.represents_alert_group,
|
||||
notification_step=phone_call_record.notification_policy.step
|
||||
if phone_call_record.notification_policy
|
||||
else None,
|
||||
notification_channel=phone_call_record.notification_policy.notify_by
|
||||
if phone_call_record.notification_policy
|
||||
else None,
|
||||
notification_step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notification_channel=UserNotificationPolicy.NotificationChannel.PHONE_CALL,
|
||||
)
|
||||
log_record.save()
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.2.10 on 2024-07-03 08:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('phone_notifications', '0002_bannedphonenumber'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='smsrecord',
|
||||
name='represents_bundle_uuid',
|
||||
field=models.CharField(db_index=True, default=None, max_length=100, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -45,7 +45,7 @@ class SMSRecord(models.Model):
|
|||
notification_policy = models.ForeignKey(
|
||||
"base.UserNotificationPolicy", on_delete=models.SET_NULL, null=True, default=None
|
||||
)
|
||||
|
||||
represents_bundle_uuid = models.CharField(max_length=100, null=True, default=None, db_index=True)
|
||||
receiver = models.ForeignKey("user_management.User", on_delete=models.CASCADE, null=True, default=None)
|
||||
grafana_cloud_notification = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from apps.alerts.incident_appearance.renderers.phone_call_renderer import AlertGroupPhoneCallRenderer
|
||||
from apps.alerts.incident_appearance.renderers.sms_renderer import AlertGroupSmsRenderer
|
||||
from apps.alerts.incident_appearance.renderers.sms_renderer import AlertGroupSMSBundleRenderer, AlertGroupSmsRenderer
|
||||
from apps.alerts.signals import user_notification_action_triggered_signal
|
||||
from apps.base.utils import live_settings
|
||||
from common.api_helpers.utils import create_engine_url
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
from common.utils import clean_markup
|
||||
|
||||
from .exceptions import (
|
||||
|
|
@ -27,6 +28,19 @@ from .phone_provider import PhoneProvider, get_phone_provider
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(
|
||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
||||
)
|
||||
def notify_by_sms_bundle_async_task(user_id, bundle_uuid):
|
||||
from apps.user_management.models import User
|
||||
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
if not user:
|
||||
return
|
||||
phone_backend = PhoneBackend()
|
||||
phone_backend.notify_by_sms_bundle(user, bundle_uuid)
|
||||
|
||||
|
||||
class PhoneBackend:
|
||||
def __init__(self):
|
||||
self.phone_provider: PhoneProvider = self._get_phone_provider()
|
||||
|
|
@ -148,16 +162,90 @@ class PhoneBackend:
|
|||
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
|
||||
log_record_error_code = None
|
||||
|
||||
renderer = AlertGroupSmsRenderer(alert_group)
|
||||
message = renderer.render()
|
||||
_, log_record_error_code = self._send_sms(
|
||||
user=user,
|
||||
alert_group=alert_group,
|
||||
notification_policy=notification_policy,
|
||||
message=message,
|
||||
)
|
||||
|
||||
if log_record_error_code is not None:
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
notification_policy=notification_policy,
|
||||
alert_group=alert_group,
|
||||
notification_error_code=log_record_error_code,
|
||||
notification_step=notification_policy.step if notification_policy else None,
|
||||
notification_channel=notification_policy.notify_by if notification_policy else None,
|
||||
)
|
||||
log_record.save()
|
||||
user_notification_action_triggered_signal.send(sender=PhoneBackend.notify_by_sms, log_record=log_record)
|
||||
|
||||
@staticmethod
|
||||
def notify_by_sms_bundle_async(user, bundle_uuid):
|
||||
notify_by_sms_bundle_async_task.apply_async((user.id, bundle_uuid))
|
||||
|
||||
def notify_by_sms_bundle(self, user, bundle_uuid):
|
||||
"""
|
||||
notify_by_sms_bundle sends an sms notification bundle to a user using configured phone provider.
|
||||
It handles business logic - limits, cloud notifications and UserNotificationPolicyLogRecord creation.
|
||||
It creates UserNotificationPolicyLogRecord for every notification in bundle, but only one SMSRecord.
|
||||
SMS itself is handled by phone provider.
|
||||
"""
|
||||
|
||||
from apps.alerts.models import BundledNotification
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
|
||||
notifications = BundledNotification.objects.filter(bundle_uuid=bundle_uuid).select_related("alert_group")
|
||||
|
||||
if not notifications:
|
||||
logger.info("Notification bundle is empty, related alert groups might have been deleted")
|
||||
return
|
||||
renderer = AlertGroupSMSBundleRenderer(notifications)
|
||||
message = renderer.render()
|
||||
|
||||
_, log_record_error_code = self._send_sms(user=user, message=message, bundle_uuid=bundle_uuid)
|
||||
|
||||
if log_record_error_code is not None:
|
||||
log_records_to_create = []
|
||||
for notification in notifications:
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
notification_policy=notification.notification_policy,
|
||||
alert_group=notification.alert_group,
|
||||
notification_error_code=log_record_error_code,
|
||||
notification_step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notification_channel=UserNotificationPolicy.NotificationChannel.SMS,
|
||||
)
|
||||
log_records_to_create.append(log_record)
|
||||
if log_records_to_create:
|
||||
if log_record_error_code in UserNotificationPolicyLogRecord.ERRORS_TO_SEND_IN_SLACK_CHANNEL:
|
||||
# create last log record outside of the bulk_create to get it as an object to send
|
||||
# the user_notification_action_triggered_signal
|
||||
log_record = log_records_to_create.pop()
|
||||
log_record.save()
|
||||
user_notification_action_triggered_signal.send(
|
||||
sender=PhoneBackend.notify_by_sms_bundle, log_record=log_record
|
||||
)
|
||||
|
||||
UserNotificationPolicyLogRecord.objects.bulk_create(log_records_to_create, batch_size=5000)
|
||||
|
||||
def _send_sms(
|
||||
self, user, message, alert_group=None, notification_policy=None, bundle_uuid=None
|
||||
) -> Tuple[bool, Optional[int]]:
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
|
||||
log_record_error_code = None
|
||||
record = SMSRecord(
|
||||
represents_alert_group=alert_group,
|
||||
receiver=user,
|
||||
notification_policy=notification_policy,
|
||||
exceeded_limit=False,
|
||||
represents_bundle_uuid=bundle_uuid,
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
@ -180,22 +268,7 @@ class PhoneBackend:
|
|||
except NumberNotVerified:
|
||||
log_record_error_code = UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_PHONE_NUMBER_IS_NOT_VERIFIED
|
||||
|
||||
if log_record_error_code is not None:
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
notification_policy=notification_policy,
|
||||
alert_group=alert_group,
|
||||
notification_error_code=log_record_error_code,
|
||||
notification_step=notification_policy.step if notification_policy else None,
|
||||
notification_channel=notification_policy.notify_by if notification_policy else None,
|
||||
)
|
||||
log_record.save()
|
||||
user_notification_action_triggered_signal.send(sender=PhoneBackend.notify_by_sms, log_record=log_record)
|
||||
|
||||
@staticmethod
|
||||
def notify_by_sms_bundle_async(user, bundle_uuid):
|
||||
pass # todo: will be added in a separate PR
|
||||
return log_record_error_code is None, log_record_error_code
|
||||
|
||||
def _notify_by_provider_sms(self, user, message) -> Optional[ProviderSMS]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from unittest import mock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
from apps.phone_notifications.exceptions import (
|
||||
|
|
@ -234,3 +236,62 @@ def test_notify_by_cloud_sms_handles_exceptions_from_cloud(
|
|||
).count()
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notify_by_sms_bundle(
|
||||
make_organization_and_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_user_notification_bundle,
|
||||
make_user_notification_policy,
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group_1 = make_alert_group(alert_receive_channel)
|
||||
alert_group_2 = make_alert_group(alert_receive_channel)
|
||||
notification_policy = make_user_notification_policy(
|
||||
user=user,
|
||||
step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notify_by=UserNotificationPolicy.NotificationChannel.SMS,
|
||||
)
|
||||
notification_bundle = make_user_notification_bundle(
|
||||
user, UserNotificationPolicy.NotificationChannel.SMS, notification_task_id="test_task_id", eta=timezone.now()
|
||||
)
|
||||
notification_bundle.append_notification(alert_group_1, notification_policy)
|
||||
notification_bundle.append_notification(alert_group_2, notification_policy)
|
||||
|
||||
bundle_uuid = "test_notifications_bundle"
|
||||
|
||||
notification_bundle.notifications.update(bundle_uuid=bundle_uuid)
|
||||
|
||||
assert not user.personal_log_records.exists()
|
||||
assert not user.smsrecord_set.exists()
|
||||
|
||||
with patch(
|
||||
"apps.phone_notifications.phone_backend.PhoneBackend._notify_by_cloud_sms", side_effect=SMSLimitExceeded
|
||||
):
|
||||
phone_backend = PhoneBackend()
|
||||
phone_backend.notify_by_sms_bundle(user, bundle_uuid)
|
||||
|
||||
# check that 2 error log records (1 for each bundled notification) and 1 sms record have been created
|
||||
assert (
|
||||
user.personal_log_records.filter(
|
||||
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_SMS_LIMIT_EXCEEDED
|
||||
).count()
|
||||
== notification_bundle.notifications.count()
|
||||
== 2
|
||||
)
|
||||
assert user.smsrecord_set.count() == 1
|
||||
|
||||
with patch("apps.phone_notifications.phone_backend.PhoneBackend._notify_by_cloud_sms"):
|
||||
phone_backend = PhoneBackend()
|
||||
phone_backend.notify_by_sms_bundle(user, bundle_uuid)
|
||||
|
||||
# check that 0 new error log records and 1 new sms record have been created
|
||||
assert (
|
||||
user.personal_log_records.filter(notification_error_code__isnull=False).count()
|
||||
== notification_bundle.notifications.count()
|
||||
== 2
|
||||
)
|
||||
assert user.smsrecord_set.count() == 2
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import logging
|
|||
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.alerts.signals import user_notification_action_triggered_signal
|
||||
from apps.alerts.models import BundledNotification
|
||||
from apps.alerts.tasks import send_update_log_report_signal
|
||||
from apps.twilioapp.models import TwilioCallStatuses, TwilioPhoneCall, TwilioSMS, TwilioSMSstatuses
|
||||
from common.api_helpers.utils import create_engine_url
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ def update_twilio_call_status(call_sid, call_status):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
|
||||
if call_sid and call_status:
|
||||
logger.info(f"twilioapp.update_twilio_call_status: processing sid={call_sid} status={call_status}")
|
||||
|
|
@ -68,19 +69,14 @@ def update_twilio_call_status(call_sid, call_status):
|
|||
author=phone_call_record.receiver,
|
||||
notification_policy=phone_call_record.notification_policy,
|
||||
alert_group=phone_call_record.represents_alert_group,
|
||||
notification_step=phone_call_record.notification_policy.step
|
||||
if phone_call_record.notification_policy
|
||||
else None,
|
||||
notification_channel=phone_call_record.notification_policy.notify_by
|
||||
if phone_call_record.notification_policy
|
||||
else None,
|
||||
notification_step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notification_channel=UserNotificationPolicy.NotificationChannel.PHONE_CALL,
|
||||
)
|
||||
log_record.save()
|
||||
logger.info(
|
||||
f"twilioapp.update_twilio_call_status: created log_record log_record_id={log_record.id} "
|
||||
f"type={log_record_type}"
|
||||
)
|
||||
user_notification_action_triggered_signal.send(sender=update_twilio_call_status, log_record=log_record)
|
||||
|
||||
|
||||
def get_error_code_by_twilio_status(status):
|
||||
|
|
@ -106,7 +102,7 @@ def update_twilio_sms_status(message_sid, message_status):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
|
||||
if message_sid and message_status:
|
||||
logger.info(f"twilioapp.update_twilio_message_status: processing sid={message_sid} status={message_status}")
|
||||
|
|
@ -143,23 +139,44 @@ def update_twilio_sms_status(message_sid, message_status):
|
|||
log_record_type = UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
||||
log_record_error_code = get_sms_error_code_by_twilio_status(status_code)
|
||||
if log_record_type is not None:
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
type=log_record_type,
|
||||
notification_error_code=log_record_error_code,
|
||||
author=sms_record.receiver,
|
||||
notification_policy=sms_record.notification_policy,
|
||||
alert_group=sms_record.represents_alert_group,
|
||||
notification_step=sms_record.notification_policy.step if sms_record.notification_policy else None,
|
||||
notification_channel=sms_record.notification_policy.notify_by
|
||||
if sms_record.notification_policy
|
||||
else None,
|
||||
)
|
||||
log_record.save()
|
||||
logger.info(
|
||||
f"twilioapp.update_twilio_sms_status: created log_record log_record_id={log_record.id} "
|
||||
f"type={log_record_type}"
|
||||
)
|
||||
user_notification_action_triggered_signal.send(sender=update_twilio_sms_status, log_record=log_record)
|
||||
if sms_record.represents_bundle_uuid:
|
||||
notifications = BundledNotification.objects.filter(bundle_uuid=sms_record.represents_bundle_uuid)
|
||||
log_records_to_create = []
|
||||
for notification in notifications:
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
type=log_record_type,
|
||||
notification_error_code=log_record_error_code,
|
||||
author=sms_record.receiver,
|
||||
notification_policy=notification.notification_policy,
|
||||
alert_group=notification.alert_group,
|
||||
notification_step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notification_channel=UserNotificationPolicy.NotificationChannel.SMS,
|
||||
)
|
||||
log_records_to_create.append(log_record)
|
||||
# send send_update_log_report_signal with 10 seconds delay
|
||||
send_update_log_report_signal.apply_async(
|
||||
kwargs={"alert_group_pk": notification.alert_group_id}, countdown=10
|
||||
)
|
||||
UserNotificationPolicyLogRecord.objects.bulk_create(log_records_to_create, batch_size=5000)
|
||||
logger.info(
|
||||
f"twilioapp.update_twilio_sms_status: created log_records for sms bundle "
|
||||
f"{sms_record.represents_bundle_uuid} type={log_record_type}"
|
||||
)
|
||||
else:
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
type=log_record_type,
|
||||
notification_error_code=log_record_error_code,
|
||||
author=sms_record.receiver,
|
||||
notification_policy=sms_record.notification_policy,
|
||||
alert_group=sms_record.represents_alert_group,
|
||||
notification_step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notification_channel=UserNotificationPolicy.NotificationChannel.SMS,
|
||||
)
|
||||
log_record.save()
|
||||
logger.info(
|
||||
f"twilioapp.update_twilio_sms_status: created log_record log_record_id={log_record.id} "
|
||||
f"type={log_record_type}"
|
||||
)
|
||||
|
||||
|
||||
def get_sms_error_code_by_twilio_status(status):
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from unittest import mock
|
|||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.http import urlencode
|
||||
from rest_framework.test import APIClient
|
||||
|
|
@ -101,3 +102,64 @@ def test_update_status(mock_has_permission, mock_slack_api_call, make_twilio_sms
|
|||
assert response.data == ""
|
||||
twilio_sms.refresh_from_db()
|
||||
assert twilio_sms.status == TwilioSMSstatuses.DETERMINANT[status]
|
||||
|
||||
|
||||
@mock.patch("apps.twilioapp.views.AllowOnlyTwilio.has_permission")
|
||||
@pytest.mark.django_db
|
||||
def test_update_status_for_bundled_notifications(
|
||||
mock_has_permission,
|
||||
mock_slack_api_call,
|
||||
make_organization_and_user,
|
||||
make_alert_receive_channel,
|
||||
make_user_notification_policy,
|
||||
make_user_notification_bundle,
|
||||
make_alert_group,
|
||||
make_sms_record,
|
||||
):
|
||||
"""The test for SMSMessage status update via api for notification bundle"""
|
||||
organization, user = make_organization_and_user()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group_1 = make_alert_group(alert_receive_channel)
|
||||
alert_group_2 = make_alert_group(alert_receive_channel)
|
||||
notification_policy = make_user_notification_policy(
|
||||
user=user,
|
||||
step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notify_by=UserNotificationPolicy.NotificationChannel.SMS,
|
||||
)
|
||||
|
||||
notification_bundle = make_user_notification_bundle(
|
||||
user, UserNotificationPolicy.NotificationChannel.SMS, notification_task_id="test_task_id", eta=timezone.now()
|
||||
)
|
||||
notification_bundle.append_notification(alert_group_1, notification_policy)
|
||||
notification_bundle.append_notification(alert_group_2, notification_policy)
|
||||
bundle_uuid = "test_notifications_bundle"
|
||||
|
||||
notification_bundle.notifications.update(bundle_uuid=bundle_uuid)
|
||||
sms_record = make_sms_record(
|
||||
receiver=user,
|
||||
represents_bundle_uuid=bundle_uuid,
|
||||
notification_policy=notification_policy,
|
||||
)
|
||||
twilio_sms = TwilioSMS.objects.create(sid="SMa12312312a123a123123c6dd2f1aee77", sms_record=sms_record)
|
||||
|
||||
mock_has_permission.return_value = True
|
||||
status = "delivered"
|
||||
data = {
|
||||
"MessageSid": twilio_sms.sid,
|
||||
"MessageStatus": status,
|
||||
"AccountSid": "Because of mock_has_permission there are may be any value",
|
||||
}
|
||||
assert user.personal_log_records.count() == 0
|
||||
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
path=reverse("twilioapp:sms_status_events"),
|
||||
data=urlencode(MultiValueDict(data), doseq=True),
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
assert response.status_code == 204
|
||||
assert response.data == ""
|
||||
twilio_sms.refresh_from_db()
|
||||
assert twilio_sms.status == TwilioSMSstatuses.DETERMINANT[status]
|
||||
|
||||
assert user.personal_log_records.count() == 2
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def update_zvonok_call_status(call_id: str, call_status: str, user_choice: Optional[str] = None):
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
|
||||
status_code = ZvonokCallStatuses.DETERMINANT.get(call_status)
|
||||
if status_code is None:
|
||||
|
|
@ -57,12 +57,8 @@ def update_zvonok_call_status(call_id: str, call_status: str, user_choice: Optio
|
|||
author=phone_call_record.receiver,
|
||||
notification_policy=phone_call_record.notification_policy,
|
||||
alert_group=phone_call_record.represents_alert_group,
|
||||
notification_step=phone_call_record.notification_policy.step
|
||||
if phone_call_record.notification_policy
|
||||
else None,
|
||||
notification_channel=phone_call_record.notification_policy.notify_by
|
||||
if phone_call_record.notification_policy
|
||||
else None,
|
||||
notification_step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notification_channel=UserNotificationPolicy.NotificationChannel.PHONE_CALL,
|
||||
)
|
||||
log_record.save()
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ CELERY_TASK_ROUTES = {
|
|||
"queue": "critical"
|
||||
},
|
||||
"apps.mobile_app.fcm_relay.fcm_relay_async": {"queue": "critical"},
|
||||
"apps.phone_notifications.phone_backend.notify_by_sms_bundle_async_task": {"queue": "critical"},
|
||||
"apps.schedules.tasks.drop_cached_ical.drop_cached_ical_for_custom_events_for_organization": {"queue": "critical"},
|
||||
"apps.schedules.tasks.drop_cached_ical.drop_cached_ical_task": {"queue": "critical"},
|
||||
# GRAFANA
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue