# What this PR does This PR moves phone notification logic into separate object PhoneBackend and introduces PhoneProvider interface to hide actual implementation of external phone services provider. It should allow add new phone providers just by implementing one class (See SimplePhoneProvider for example). # Why [Asterisk PR](https://github.com/grafana/oncall/pull/1282) showed that our phone notification system is not flexible. However this is one of the most frequent community questions - how to add "X" phone provider. Also, this refactoring move us one step closer to unifying all notification backends, since with PhoneBackend all phone notification logic is collected in one place and independent from concrete realisation. # Highligts 1. PhoneBackend object - contains all phone notifications business logic. 2. PhoneProvider - interface to external phone services provider. 3. TwilioPhoneProvider and SimplePhoneProvider - two examples of PhoneProvider implementation. 4. PhoneCallRecord and SMSRecord models. I introduced these models to keep phone notification limits logic decoupled from external providers. Existing TwilioPhoneCall and TwilioSMS objects will be migrated to the new table to not to reset limits counter. To be able to receive status callbacks and gather from Twilio TwilioPhoneCall and TwilioSMS still exists, but they are linked to PhoneCallRecord and SMSRecord via fk, to not to leat twilio logic into core code. --------- Co-authored-by: Yulia Shanyrova <yulia.shanyrova@grafana.com>
168 lines
6.2 KiB
Python
168 lines
6.2 KiB
Python
from dataclasses import asdict
|
|
from datetime import timedelta
|
|
|
|
import humanize
|
|
import pytz
|
|
from django.apps import apps
|
|
from django.utils import timezone
|
|
from rest_framework import fields, serializers
|
|
|
|
from apps.base.models import LiveSetting
|
|
from apps.phone_notifications.phone_provider import get_phone_provider
|
|
from apps.slack.models import SlackTeamIdentity
|
|
from apps.slack.tasks import resolve_archived_incidents_for_organization, unarchive_incidents_for_organization
|
|
from apps.user_management.models import Organization
|
|
from common.api_helpers.mixins import EagerLoadingMixin
|
|
|
|
|
|
class CustomDateField(fields.TimeField):
|
|
def to_internal_value(self, data):
|
|
try:
|
|
archive_datetime = timezone.datetime.fromisoformat(data).astimezone(pytz.UTC)
|
|
except (TypeError, ValueError):
|
|
raise serializers.ValidationError({"archive_alerts_from": ["Invalid date format"]})
|
|
if archive_datetime.date() >= timezone.now().date():
|
|
raise serializers.ValidationError({"archive_alerts_from": ["Invalid date. Date must be less than today."]})
|
|
return archive_datetime
|
|
|
|
|
|
class FastSlackTeamIdentitySerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = SlackTeamIdentity
|
|
fields = ["cached_name"]
|
|
|
|
|
|
class OrganizationSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|
pk = serializers.CharField(read_only=True, source="public_primary_key")
|
|
slack_team_identity = FastSlackTeamIdentitySerializer(read_only=True)
|
|
|
|
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, source="org_title")
|
|
# name_slug = serializers.CharField(required=False, allow_null=True, allow_blank=False)
|
|
maintenance_till = serializers.ReadOnlyField(source="till_maintenance_timestamp")
|
|
slack_channel = serializers.SerializerMethodField()
|
|
|
|
SELECT_RELATED = ["slack_team_identity"]
|
|
|
|
class Meta:
|
|
model = Organization
|
|
fields = [
|
|
"pk",
|
|
"name",
|
|
# "name_slug",
|
|
# "is_new_version",
|
|
"slack_team_identity",
|
|
"maintenance_mode",
|
|
"maintenance_till",
|
|
# "incident_retention_web_report",
|
|
# "number_of_employees",
|
|
"slack_channel",
|
|
]
|
|
read_only_fields = [
|
|
"is_new_version",
|
|
"slack_team_identity",
|
|
"maintenance_mode",
|
|
"maintenance_till",
|
|
# "incident_retention_web_report",
|
|
]
|
|
|
|
def get_slack_channel(self, obj):
|
|
SlackChannel = apps.get_model("slack", "SlackChannel")
|
|
if obj.general_log_channel_id is None or obj.slack_team_identity is None:
|
|
return None
|
|
try:
|
|
channel = obj.slack_team_identity.get_cached_channels().get(slack_id=obj.general_log_channel_id)
|
|
except SlackChannel.DoesNotExist:
|
|
return {"display_name": None, "slack_id": obj.general_log_channel_id, "id": None}
|
|
|
|
return {
|
|
"display_name": channel.name,
|
|
"slack_id": channel.slack_id,
|
|
"id": channel.public_primary_key,
|
|
}
|
|
|
|
|
|
class CurrentOrganizationSerializer(OrganizationSerializer):
|
|
limits = serializers.SerializerMethodField()
|
|
env_status = serializers.SerializerMethodField()
|
|
banner = serializers.SerializerMethodField()
|
|
|
|
class Meta(OrganizationSerializer.Meta):
|
|
fields = [
|
|
*OrganizationSerializer.Meta.fields,
|
|
"limits",
|
|
"archive_alerts_from",
|
|
"is_resolution_note_required",
|
|
"env_status",
|
|
"banner",
|
|
]
|
|
read_only_fields = [
|
|
*OrganizationSerializer.Meta.read_only_fields,
|
|
"limits",
|
|
"banner",
|
|
]
|
|
|
|
def get_banner(self, obj):
|
|
DynamicSetting = apps.get_model("base", "DynamicSetting")
|
|
banner = DynamicSetting.objects.get_or_create(
|
|
name="banner",
|
|
defaults={"json_value": {"title": None, "body": None}},
|
|
)[0]
|
|
return banner.json_value
|
|
|
|
def get_limits(self, obj):
|
|
user = self.context["request"].user
|
|
return obj.notifications_limit_web_report(user)
|
|
|
|
def get_env_status(self, obj):
|
|
# deprecated in favour of ConfigAPIView.
|
|
# All new env statuses should be added there
|
|
LiveSetting.populate_settings_if_needed()
|
|
|
|
telegram_configured = not LiveSetting.objects.filter(name__startswith="TELEGRAM", error__isnull=False).exists()
|
|
phone_provider_config = get_phone_provider().flags
|
|
return {
|
|
"telegram_configured": telegram_configured,
|
|
"twilio_configured": phone_provider_config.configured, # keep for backward compatibility
|
|
"phone_provider": asdict(phone_provider_config),
|
|
}
|
|
|
|
def get_stats(self, obj):
|
|
if isinstance(obj.cached_seconds_saved_by_amixr, int):
|
|
verbal_time_saved_by_amixr = humanize.naturaldelta(timedelta(seconds=obj.cached_seconds_saved_by_amixr))
|
|
else:
|
|
verbal_time_saved_by_amixr = None
|
|
|
|
result = {
|
|
"grouped_percent": obj.cached_grouped_percent,
|
|
"alerts_count": obj.cached_alerts_count,
|
|
"noise_reduction": obj.cached_noise_reduction,
|
|
"average_response_time": humanize.naturaldelta(obj.cached_average_response_time),
|
|
"verbal_time_saved_by_amixr": verbal_time_saved_by_amixr,
|
|
}
|
|
|
|
return result
|
|
|
|
def update(self, instance, validated_data):
|
|
current_archive_date = instance.archive_alerts_from
|
|
archive_alerts_from = validated_data.get("archive_alerts_from")
|
|
|
|
result = super().update(instance, validated_data)
|
|
if archive_alerts_from is not None and current_archive_date != archive_alerts_from:
|
|
if current_archive_date > archive_alerts_from:
|
|
unarchive_incidents_for_organization.apply_async(
|
|
(instance.pk,),
|
|
)
|
|
resolve_archived_incidents_for_organization.apply_async(
|
|
(instance.pk,),
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
class FastOrganizationSerializer(serializers.ModelSerializer):
|
|
pk = serializers.CharField(read_only=True, source="public_primary_key")
|
|
name = serializers.CharField(read_only=True, source="org_title")
|
|
|
|
class Meta:
|
|
model = Organization
|
|
fields = ["pk", "name"]
|