oncall-engine/engine/apps/api/serializers/organization.py
Innokentii Konstantinov 1f786e8d2a
Phone provider refactoring (#1713)
# 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>
2023-05-24 06:27:48 +00:00

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"]