Skip to content

Commit 52ae155

Browse files
Merge pull request #219 from grafana/dev
Merge dev to main
2 parents 56b4e12 + 35a3170 commit 52ae155

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2598
-841
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
## v1.0.5 (2022-07-12)
4+
5+
- Manual Incidents enabled for teams
6+
- Fix phone notifications for OSS
7+
- Public API improvements
8+
39
## 1.0.4 (2022-06-28)
410
- Allow Telegram DMs without channel connection.
511

engine/apps/alerts/incident_appearance/renderers/web_renderer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def render(self):
1414
"title": str_or_backup(templated_alert.title, "Alert"),
1515
"message": str_or_backup(templated_alert.message, ""),
1616
"image_url": str_or_backup(templated_alert.image_url, None),
17-
"source_link": str_or_backup(templated_alert.image_url, None),
17+
"source_link": str_or_backup(templated_alert.source_link, None),
1818
}
1919
return rendered_alert
2020

engine/apps/alerts/incident_appearance/templaters/slack_templater.py

+54
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from django.apps import apps
2+
13
from apps.alerts.incident_appearance.templaters.alert_templater import AlertTemplater
24

35

@@ -12,3 +14,55 @@ def _postformat(self, templated_alert):
1214
if templated_alert.title:
1315
templated_alert.title = templated_alert.title.replace("\n", "").replace("\r", "")
1416
return templated_alert
17+
18+
def render(self):
19+
"""
20+
Overriden render method to modify payload of manual integration alerts
21+
"""
22+
self._modify_payload_for_manual_integration_if_needed()
23+
return super().render()
24+
25+
def _modify_payload_for_manual_integration_if_needed(self):
26+
"""
27+
Modifies payload of alerts made from manual incident integration.
28+
It is needed to simplify templates.
29+
"""
30+
payload = self.alert.raw_request_data
31+
# First check if payload look like payload from manual incident integration and was not modified before.
32+
if "view" in payload and "private_metadata" in payload.get("view", {}) and "oncall" not in payload:
33+
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
34+
# If so - check it with db query.
35+
if self.alert.group.channel.integration == AlertReceiveChannel.INTEGRATION_MANUAL:
36+
metadata = payload.get("view", {}).get("private_metadata", {})
37+
payload["oncall"] = {}
38+
if "message" in metadata:
39+
# If alert was made from message
40+
domain = payload.get("team", {}).get("domain", "unknown")
41+
channel_id = metadata.get("channel_id", "unknown")
42+
message = metadata.get("message", {})
43+
message_ts = message.get("ts", "unknown")
44+
message_text = message.get("text", "unknown")
45+
payload["oncall"]["permalink"] = f"https://{domain}.slack.com/archives/{channel_id}/p{message_ts}"
46+
payload["oncall"]["author_username"] = metadata.get("author_username", "Unknown")
47+
payload["oncall"]["title"] = "Message from @" + payload["oncall"]["author_username"]
48+
payload["oncall"]["message"] = message_text
49+
else:
50+
# If alert was made via slash command
51+
message_text = (
52+
payload.get("view", {})
53+
.get("state", {})
54+
.get("values", {})
55+
.get("MESSAGE_INPUT", {})
56+
.get("FinishCreateIncidentViewStep", {})
57+
.get("value", "unknown")
58+
)
59+
payload["oncall"]["permalink"] = None
60+
payload["oncall"]["title"] = self.alert.title
61+
payload["oncall"]["message"] = message_text
62+
created_by = self.alert.integration_unique_data.get("created_by", None)
63+
username = payload.get("user", {}).get("name", None)
64+
author_username = created_by or username or "unknown"
65+
payload["oncall"]["author_username"] = author_username
66+
67+
self.alert.raw_request_data = payload
68+
self.alert.save(update_fields=["raw_request_data"])

engine/apps/alerts/incident_appearance/templaters/web_templater.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ def _postformat(self, templated_alert):
3232

3333
def _slack_format_for_web(self, data):
3434
sf = self.slack_formatter
35-
sf.hyperlink_mention_format = "[title](url)"
35+
sf.hyperlink_mention_format = "[{title}]({url})"
3636
return sf.format(data)

engine/apps/alerts/models/alert_receive_channel.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,9 @@ def description(self):
448448
def get_or_create_manual_integration(cls, defaults, **kwargs):
449449
try:
450450
alert_receive_channel = cls.objects.get(
451-
organization=kwargs["organization"], integration=kwargs["integration"]
451+
organization=kwargs["organization"],
452+
integration=kwargs["integration"],
453+
team=kwargs["team"],
452454
)
453455
except cls.DoesNotExist:
454456
kwargs.update(defaults)

engine/apps/api/serializers/channel_filter.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def validate_filtering_term(self, filtering_term):
9393
def validate_notification_backends(self, notification_backends):
9494
# NOTE: updates the whole field, handling dict updates per backend
9595
if notification_backends is not None:
96+
organization = self.context["request"].auth.organization
9697
if not isinstance(notification_backends, dict):
9798
raise serializers.ValidationError(["Invalid messaging backend data"])
9899
current = self.instance.notification_backends or {}
@@ -101,7 +102,7 @@ def validate_notification_backends(self, notification_backends):
101102
if backend is None:
102103
raise serializers.ValidationError(["Invalid messaging backend"])
103104
updated_data = backend.validate_channel_filter_data(
104-
self.instance,
105+
organization,
105106
notification_backends[backend_id],
106107
)
107108
# update existing backend data

engine/apps/api/serializers/schedule_polymorphic.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
ScheduleICalSerializer,
77
ScheduleICalUpdateSerializer,
88
)
9-
from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal
9+
from apps.api.serializers.schedule_web import ScheduleWebCreateSerializer, ScheduleWebSerializer
10+
from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal, OnCallScheduleWeb
1011
from common.api_helpers.mixins import EagerLoadingMixin
1112

1213

@@ -18,9 +19,10 @@ class PolymorphicScheduleSerializer(EagerLoadingMixin, PolymorphicSerializer):
1819
model_serializer_mapping = {
1920
OnCallScheduleICal: ScheduleICalSerializer,
2021
OnCallScheduleCalendar: ScheduleCalendarSerializer,
22+
OnCallScheduleWeb: ScheduleWebSerializer,
2123
}
2224

23-
SCHEDULE_CLASS_TO_TYPE = {OnCallScheduleCalendar: 0, OnCallScheduleICal: 1}
25+
SCHEDULE_CLASS_TO_TYPE = {OnCallScheduleCalendar: 0, OnCallScheduleICal: 1, OnCallScheduleWeb: 2}
2426

2527
def to_resource_type(self, model_or_instance):
2628
return self.SCHEDULE_CLASS_TO_TYPE.get(model_or_instance._meta.model)
@@ -31,6 +33,7 @@ class PolymorphicScheduleCreateSerializer(PolymorphicScheduleSerializer):
3133
model_serializer_mapping = {
3234
OnCallScheduleICal: ScheduleICalCreateSerializer,
3335
OnCallScheduleCalendar: ScheduleCalendarCreateSerializer,
36+
OnCallScheduleWeb: ScheduleWebCreateSerializer,
3437
}
3538

3639

@@ -39,4 +42,5 @@ class PolymorphicScheduleUpdateSerializer(PolymorphicScheduleSerializer):
3942
OnCallScheduleICal: ScheduleICalUpdateSerializer,
4043
# There is no difference between create and Update serializers for ScheduleCalendar
4144
OnCallScheduleCalendar: ScheduleCalendarCreateSerializer,
45+
OnCallScheduleWeb: ScheduleWebCreateSerializer,
4246
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from rest_framework import serializers
2+
3+
from apps.api.serializers.schedule_base import ScheduleBaseSerializer
4+
from apps.schedules.models import OnCallScheduleWeb
5+
from apps.schedules.tasks import schedule_notify_about_empty_shifts_in_schedule, schedule_notify_about_gaps_in_schedule
6+
from apps.slack.models import SlackChannel, SlackUserGroup
7+
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
8+
9+
10+
class ScheduleWebSerializer(ScheduleBaseSerializer):
11+
time_zone = serializers.CharField(required=False)
12+
13+
class Meta:
14+
model = OnCallScheduleWeb
15+
fields = [*ScheduleBaseSerializer.Meta.fields, "slack_channel", "time_zone"]
16+
17+
18+
class ScheduleWebCreateSerializer(ScheduleWebSerializer):
19+
slack_channel_id = OrganizationFilteredPrimaryKeyRelatedField(
20+
filter_field="slack_team_identity__organizations",
21+
queryset=SlackChannel.objects,
22+
required=False,
23+
allow_null=True,
24+
)
25+
user_group = OrganizationFilteredPrimaryKeyRelatedField(
26+
filter_field="slack_team_identity__organizations",
27+
queryset=SlackUserGroup.objects,
28+
required=False,
29+
allow_null=True,
30+
)
31+
32+
class Meta(ScheduleWebSerializer.Meta):
33+
fields = [*ScheduleBaseSerializer.Meta.fields, "slack_channel_id", "time_zone"]
34+
35+
def update(self, instance, validated_data):
36+
updated_schedule = super().update(instance, validated_data)
37+
38+
old_time_zone = instance.time_zone
39+
updated_time_zone = updated_schedule.time_zone
40+
if old_time_zone != updated_time_zone:
41+
updated_schedule.drop_cached_ical()
42+
updated_schedule.check_empty_shifts_for_next_week()
43+
updated_schedule.check_gaps_for_next_week()
44+
schedule_notify_about_empty_shifts_in_schedule.apply_async((instance.pk,))
45+
schedule_notify_about_gaps_in_schedule.apply_async((instance.pk,))
46+
return updated_schedule

engine/apps/api/serializers/user.py

+55
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import time
2+
3+
import pytz
14
from django.conf import settings
25
from rest_framework import serializers
36

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

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

3439
permissions = serializers.SerializerMethodField()
@@ -47,6 +52,8 @@ class Meta:
4752
"username",
4853
"role",
4954
"avatar",
55+
"timezone",
56+
"working_hours",
5057
"unverified_phone_number",
5158
"verified_phone_number",
5259
"slack_user_identity",
@@ -63,6 +70,52 @@ class Meta:
6370
"verified_phone_number",
6471
]
6572

73+
def validate_timezone(self, tz):
74+
if tz is None:
75+
return tz
76+
77+
try:
78+
pytz.timezone(tz)
79+
except pytz.UnknownTimeZoneError:
80+
raise serializers.ValidationError("not a valid timezone")
81+
82+
return tz
83+
84+
def validate_working_hours(self, working_hours):
85+
if not isinstance(working_hours, dict):
86+
raise serializers.ValidationError("must be dict")
87+
88+
# check that all days are present
89+
if sorted(working_hours.keys()) != sorted(default_working_hours().keys()):
90+
raise serializers.ValidationError("missing some days")
91+
92+
for day in working_hours:
93+
periods = working_hours[day]
94+
95+
if not isinstance(periods, list):
96+
raise serializers.ValidationError("periods must be list")
97+
98+
for period in periods:
99+
if not isinstance(period, dict):
100+
raise serializers.ValidationError("period must be dict")
101+
102+
if sorted(period.keys()) != sorted(["start", "end"]):
103+
raise serializers.ValidationError("'start' and 'end' fields must be present")
104+
105+
if not isinstance(period["start"], str) or not isinstance(period["end"], str):
106+
raise serializers.ValidationError("'start' and 'end' fields must be str")
107+
108+
try:
109+
start = time.strptime(period["start"], "%H:%M:%S")
110+
end = time.strptime(period["end"], "%H:%M:%S")
111+
except ValueError:
112+
raise serializers.ValidationError("'start' and 'end' fields must be in '%H:%M:%S' format")
113+
114+
if start >= end:
115+
raise serializers.ValidationError("'start' must be less than 'end'")
116+
117+
return working_hours
118+
66119
def validate_unverified_phone_number(self, value):
67120
if value:
68121
if check_phone_number_is_valid(value):
@@ -110,6 +163,8 @@ class UserHiddenFieldsSerializer(UserSerializer):
110163
"current_team",
111164
"username",
112165
"avatar",
166+
"timezone",
167+
"working_hours",
113168
"notification_chain_verbal",
114169
"permissions",
115170
]

0 commit comments

Comments
 (0)