Skip to content

Commit

Permalink
Merge pull request #219 from grafana/dev
Browse files Browse the repository at this point in the history
Merge dev to main
  • Loading branch information
Konstantinov-Innokentii authored Jul 12, 2022
2 parents 56b4e12 + 35a3170 commit 52ae155
Show file tree
Hide file tree
Showing 45 changed files with 2,598 additions and 841 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## v1.0.5 (2022-07-12)

- Manual Incidents enabled for teams
- Fix phone notifications for OSS
- Public API improvements

## 1.0.4 (2022-06-28)
- Allow Telegram DMs without channel connection.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def render(self):
"title": str_or_backup(templated_alert.title, "Alert"),
"message": str_or_backup(templated_alert.message, ""),
"image_url": str_or_backup(templated_alert.image_url, None),
"source_link": str_or_backup(templated_alert.image_url, None),
"source_link": str_or_backup(templated_alert.source_link, None),
}
return rendered_alert

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.apps import apps

from apps.alerts.incident_appearance.templaters.alert_templater import AlertTemplater


Expand All @@ -12,3 +14,55 @@ def _postformat(self, templated_alert):
if templated_alert.title:
templated_alert.title = templated_alert.title.replace("\n", "").replace("\r", "")
return templated_alert

def render(self):
"""
Overriden render method to modify payload of manual integration alerts
"""
self._modify_payload_for_manual_integration_if_needed()
return super().render()

def _modify_payload_for_manual_integration_if_needed(self):
"""
Modifies payload of alerts made from manual incident integration.
It is needed to simplify templates.
"""
payload = self.alert.raw_request_data
# First check if payload look like payload from manual incident integration and was not modified before.
if "view" in payload and "private_metadata" in payload.get("view", {}) and "oncall" not in payload:
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
# If so - check it with db query.
if self.alert.group.channel.integration == AlertReceiveChannel.INTEGRATION_MANUAL:
metadata = payload.get("view", {}).get("private_metadata", {})
payload["oncall"] = {}
if "message" in metadata:
# If alert was made from message
domain = payload.get("team", {}).get("domain", "unknown")
channel_id = metadata.get("channel_id", "unknown")
message = metadata.get("message", {})
message_ts = message.get("ts", "unknown")
message_text = message.get("text", "unknown")
payload["oncall"]["permalink"] = f"https://{domain}.slack.com/archives/{channel_id}/p{message_ts}"
payload["oncall"]["author_username"] = metadata.get("author_username", "Unknown")
payload["oncall"]["title"] = "Message from @" + payload["oncall"]["author_username"]
payload["oncall"]["message"] = message_text
else:
# If alert was made via slash command
message_text = (
payload.get("view", {})
.get("state", {})
.get("values", {})
.get("MESSAGE_INPUT", {})
.get("FinishCreateIncidentViewStep", {})
.get("value", "unknown")
)
payload["oncall"]["permalink"] = None
payload["oncall"]["title"] = self.alert.title
payload["oncall"]["message"] = message_text
created_by = self.alert.integration_unique_data.get("created_by", None)
username = payload.get("user", {}).get("name", None)
author_username = created_by or username or "unknown"
payload["oncall"]["author_username"] = author_username

self.alert.raw_request_data = payload
self.alert.save(update_fields=["raw_request_data"])
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ def _postformat(self, templated_alert):

def _slack_format_for_web(self, data):
sf = self.slack_formatter
sf.hyperlink_mention_format = "[title](url)"
sf.hyperlink_mention_format = "[{title}]({url})"
return sf.format(data)
4 changes: 3 additions & 1 deletion engine/apps/alerts/models/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,9 @@ def description(self):
def get_or_create_manual_integration(cls, defaults, **kwargs):
try:
alert_receive_channel = cls.objects.get(
organization=kwargs["organization"], integration=kwargs["integration"]
organization=kwargs["organization"],
integration=kwargs["integration"],
team=kwargs["team"],
)
except cls.DoesNotExist:
kwargs.update(defaults)
Expand Down
3 changes: 2 additions & 1 deletion engine/apps/api/serializers/channel_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def validate_filtering_term(self, filtering_term):
def validate_notification_backends(self, notification_backends):
# NOTE: updates the whole field, handling dict updates per backend
if notification_backends is not None:
organization = self.context["request"].auth.organization
if not isinstance(notification_backends, dict):
raise serializers.ValidationError(["Invalid messaging backend data"])
current = self.instance.notification_backends or {}
Expand All @@ -101,7 +102,7 @@ def validate_notification_backends(self, notification_backends):
if backend is None:
raise serializers.ValidationError(["Invalid messaging backend"])
updated_data = backend.validate_channel_filter_data(
self.instance,
organization,
notification_backends[backend_id],
)
# update existing backend data
Expand Down
8 changes: 6 additions & 2 deletions engine/apps/api/serializers/schedule_polymorphic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
ScheduleICalSerializer,
ScheduleICalUpdateSerializer,
)
from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal
from apps.api.serializers.schedule_web import ScheduleWebCreateSerializer, ScheduleWebSerializer
from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal, OnCallScheduleWeb
from common.api_helpers.mixins import EagerLoadingMixin


Expand All @@ -18,9 +19,10 @@ class PolymorphicScheduleSerializer(EagerLoadingMixin, PolymorphicSerializer):
model_serializer_mapping = {
OnCallScheduleICal: ScheduleICalSerializer,
OnCallScheduleCalendar: ScheduleCalendarSerializer,
OnCallScheduleWeb: ScheduleWebSerializer,
}

SCHEDULE_CLASS_TO_TYPE = {OnCallScheduleCalendar: 0, OnCallScheduleICal: 1}
SCHEDULE_CLASS_TO_TYPE = {OnCallScheduleCalendar: 0, OnCallScheduleICal: 1, OnCallScheduleWeb: 2}

def to_resource_type(self, model_or_instance):
return self.SCHEDULE_CLASS_TO_TYPE.get(model_or_instance._meta.model)
Expand All @@ -31,6 +33,7 @@ class PolymorphicScheduleCreateSerializer(PolymorphicScheduleSerializer):
model_serializer_mapping = {
OnCallScheduleICal: ScheduleICalCreateSerializer,
OnCallScheduleCalendar: ScheduleCalendarCreateSerializer,
OnCallScheduleWeb: ScheduleWebCreateSerializer,
}


Expand All @@ -39,4 +42,5 @@ class PolymorphicScheduleUpdateSerializer(PolymorphicScheduleSerializer):
OnCallScheduleICal: ScheduleICalUpdateSerializer,
# There is no difference between create and Update serializers for ScheduleCalendar
OnCallScheduleCalendar: ScheduleCalendarCreateSerializer,
OnCallScheduleWeb: ScheduleWebCreateSerializer,
}
46 changes: 46 additions & 0 deletions engine/apps/api/serializers/schedule_web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from rest_framework import serializers

from apps.api.serializers.schedule_base import ScheduleBaseSerializer
from apps.schedules.models import OnCallScheduleWeb
from apps.schedules.tasks import schedule_notify_about_empty_shifts_in_schedule, schedule_notify_about_gaps_in_schedule
from apps.slack.models import SlackChannel, SlackUserGroup
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField


class ScheduleWebSerializer(ScheduleBaseSerializer):
time_zone = serializers.CharField(required=False)

class Meta:
model = OnCallScheduleWeb
fields = [*ScheduleBaseSerializer.Meta.fields, "slack_channel", "time_zone"]


class ScheduleWebCreateSerializer(ScheduleWebSerializer):
slack_channel_id = OrganizationFilteredPrimaryKeyRelatedField(
filter_field="slack_team_identity__organizations",
queryset=SlackChannel.objects,
required=False,
allow_null=True,
)
user_group = OrganizationFilteredPrimaryKeyRelatedField(
filter_field="slack_team_identity__organizations",
queryset=SlackUserGroup.objects,
required=False,
allow_null=True,
)

class Meta(ScheduleWebSerializer.Meta):
fields = [*ScheduleBaseSerializer.Meta.fields, "slack_channel_id", "time_zone"]

def update(self, instance, validated_data):
updated_schedule = super().update(instance, validated_data)

old_time_zone = instance.time_zone
updated_time_zone = updated_schedule.time_zone
if old_time_zone != updated_time_zone:
updated_schedule.drop_cached_ical()
updated_schedule.check_empty_shifts_for_next_week()
updated_schedule.check_gaps_for_next_week()
schedule_notify_about_empty_shifts_in_schedule.apply_async((instance.pk,))
schedule_notify_about_gaps_in_schedule.apply_async((instance.pk,))
return updated_schedule
55 changes: 55 additions & 0 deletions engine/apps/api/serializers/user.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import time

import pytz
from django.conf import settings
from rest_framework import serializers

Expand All @@ -9,6 +12,7 @@
from apps.oss_installation.utils import cloud_user_identity_status
from apps.twilioapp.utils import check_phone_number_is_valid
from apps.user_management.models import User
from apps.user_management.models.user import default_working_hours
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.mixins import EagerLoadingMixin
from common.constants.role import Role
Expand All @@ -29,6 +33,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
organization = FastOrganizationSerializer(read_only=True)
current_team = TeamPrimaryKeyRelatedField(allow_null=True, required=False)

timezone = serializers.CharField(allow_null=True, required=False)
avatar = serializers.URLField(source="avatar_url", read_only=True)

permissions = serializers.SerializerMethodField()
Expand All @@ -47,6 +52,8 @@ class Meta:
"username",
"role",
"avatar",
"timezone",
"working_hours",
"unverified_phone_number",
"verified_phone_number",
"slack_user_identity",
Expand All @@ -63,6 +70,52 @@ class Meta:
"verified_phone_number",
]

def validate_timezone(self, tz):
if tz is None:
return tz

try:
pytz.timezone(tz)
except pytz.UnknownTimeZoneError:
raise serializers.ValidationError("not a valid timezone")

return tz

def validate_working_hours(self, working_hours):
if not isinstance(working_hours, dict):
raise serializers.ValidationError("must be dict")

# check that all days are present
if sorted(working_hours.keys()) != sorted(default_working_hours().keys()):
raise serializers.ValidationError("missing some days")

for day in working_hours:
periods = working_hours[day]

if not isinstance(periods, list):
raise serializers.ValidationError("periods must be list")

for period in periods:
if not isinstance(period, dict):
raise serializers.ValidationError("period must be dict")

if sorted(period.keys()) != sorted(["start", "end"]):
raise serializers.ValidationError("'start' and 'end' fields must be present")

if not isinstance(period["start"], str) or not isinstance(period["end"], str):
raise serializers.ValidationError("'start' and 'end' fields must be str")

try:
start = time.strptime(period["start"], "%H:%M:%S")
end = time.strptime(period["end"], "%H:%M:%S")
except ValueError:
raise serializers.ValidationError("'start' and 'end' fields must be in '%H:%M:%S' format")

if start >= end:
raise serializers.ValidationError("'start' must be less than 'end'")

return working_hours

def validate_unverified_phone_number(self, value):
if value:
if check_phone_number_is_valid(value):
Expand Down Expand Up @@ -110,6 +163,8 @@ class UserHiddenFieldsSerializer(UserSerializer):
"current_team",
"username",
"avatar",
"timezone",
"working_hours",
"notification_chain_verbal",
"permissions",
]
Expand Down
Loading

0 comments on commit 52ae155

Please sign in to comment.