Skip to content

Commit 686ebbf

Browse files
authored
chore: fix some minor issues with recent slack_channel changes (#5228)
# What this PR does Follow up PR to #5199 and #5224, addresses a few issues I noticed on dev while testing the feature ## 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.
1 parent 14c1b37 commit 686ebbf

12 files changed

+194
-24
lines changed

engine/apps/api/serializers/channel_filter.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from apps.alerts.models import AlertReceiveChannel, ChannelFilter, EscalationChain
44
from apps.api.serializers.labels import LabelPairSerializer
5-
from apps.api.serializers.slack_channel import SlackChannelSerializer
5+
from apps.api.serializers.slack_channel import SlackChannelDetails, SlackChannelSerializer
66
from apps.base.messaging import get_messaging_backend_from_id
77
from apps.telegram.models import TelegramToOrganizationConnector
88
from common.api_helpers.custom_fields import (
@@ -29,7 +29,7 @@ class TelegramChannelDetailsSerializer(serializers.Serializer):
2929
allow_null=True,
3030
required=False,
3131
)
32-
slack_channel = SlackChannelSerializer(read_only=True, allow_null=True)
32+
slack_channel = SlackChannelSerializer(read_only=True)
3333

3434
# TODO: we probably don't need both telegram_channel and telegram_channel_details, research which one isn't needed
3535
# and get rid of it
@@ -143,7 +143,7 @@ def get_filtering_term_as_jinja2(self, obj):
143143

144144

145145
class ChannelFilterCreateSerializer(ChannelFilterSerializer):
146-
slack_channel_id = SlackChannelsFilteredByOrganizationSlackWorkspaceField(
146+
slack_channel = SlackChannelsFilteredByOrganizationSlackWorkspaceField(
147147
allow_null=True,
148148
required=False,
149149
write_only=True,
@@ -156,7 +156,6 @@ class Meta:
156156
"alert_receive_channel",
157157
"escalation_chain",
158158
"slack_channel",
159-
"slack_channel_id",
160159
"created_at",
161160
"filtering_labels",
162161
"filtering_term",
@@ -169,6 +168,15 @@ class Meta:
169168
]
170169
read_only_fields = ["created_at", "is_default"]
171170

171+
def to_representation(self, obj):
172+
"""
173+
This feels hacky.. it's because the UI currently POST/PUTs using "slack_channel", which is the SLACK ID of
174+
the slack channel that we'd like to set it to, whereas what we return is an object with more details
175+
"""
176+
result = super().to_representation(obj)
177+
result["slack_channel"] = SlackChannelSerializer(obj.slack_channel).data if obj.slack_channel else None
178+
return result
179+
172180
def create(self, validated_data):
173181
instance = super().create(validated_data)
174182
instance.to_index(0) # the new route should be the first one
@@ -188,3 +196,15 @@ def update(self, instance, validated_data):
188196
raise BadRequest(detail="Filtering term of default channel filter cannot be changed")
189197

190198
return super().update(instance, validated_data)
199+
200+
201+
class ChannelFilterUpdateResponseSerializer(ChannelFilterUpdateSerializer):
202+
"""
203+
This serializer is used in OpenAPI schema to show proper response structure,
204+
as `slack_channel` field expects string on create/update and returns dict on response
205+
"""
206+
207+
slack_channel = serializers.SerializerMethodField()
208+
209+
def get_slack_channel(self, obj) -> SlackChannelDetails | None:
210+
...

engine/apps/api/serializers/organization.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class OrganizationSerializer(EagerLoadingMixin, serializers.ModelSerializer):
2222
slack_team_identity = FastSlackTeamIdentitySerializer(read_only=True)
2323

2424
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, source="org_title")
25-
slack_channel = SlackChannelSerializer(read_only=True, allow_null=True, required=False)
25+
slack_channel = SlackChannelSerializer(read_only=True, source="default_slack_channel")
2626

2727
rbac_enabled = serializers.BooleanField(read_only=True, source="is_rbac_permissions_enabled")
2828
grafana_incident_enabled = serializers.BooleanField(read_only=True, source="is_grafana_incident_enabled")

engine/apps/api/serializers/schedule_base.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ScheduleBaseSerializer(EagerLoadingMixin, serializers.ModelSerializer):
1313
id = serializers.CharField(read_only=True, source="public_primary_key")
1414
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
1515
team = TeamPrimaryKeyRelatedField(allow_null=True, required=False)
16-
slack_channel = SlackChannelSerializer(read_only=True, allow_null=True, required=False)
16+
slack_channel = SlackChannelSerializer(read_only=True)
1717
user_group = UserGroupSerializer()
1818
warnings = serializers.SerializerMethodField()
1919
on_call_now = serializers.SerializerMethodField()
@@ -75,10 +75,8 @@ def get_enable_web_overrides(self, obj):
7575

7676
def validate(self, attrs):
7777
if "slack_channel_id" in attrs:
78-
# this is set by OrganizationFilteredPrimaryKeyRelatedField in the serializer classes
79-
# that subclass ScheduleBaseSerializer
80-
slack_channel = attrs.pop("slack_channel_id", None)
81-
attrs["slack_channel"] = slack_channel
78+
# this is set in the serializer classes which subclass ScheduleBaseSerializer
79+
attrs["slack_channel"] = attrs.pop("slack_channel_id", None)
8280
return attrs
8381

8482
def create(self, validated_data):

engine/apps/api/serializers/schedule_calendar.py

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class ScheduleCalendarCreateSerializer(ScheduleCalendarSerializer):
2626
queryset=SlackChannel.objects,
2727
required=False,
2828
allow_null=True,
29+
write_only=True,
2930
)
3031
user_group = OrganizationFilteredPrimaryKeyRelatedField(
3132
filter_field="slack_team_identity__organizations",

engine/apps/api/serializers/schedule_ical.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ScheduleICalCreateSerializer(ScheduleICalSerializer):
3333
queryset=SlackChannel.objects,
3434
required=False,
3535
allow_null=True,
36+
write_only=True,
3637
)
3738
user_group = OrganizationFilteredPrimaryKeyRelatedField(
3839
filter_field="slack_team_identity__organizations",
@@ -41,12 +42,6 @@ class ScheduleICalCreateSerializer(ScheduleICalSerializer):
4142
allow_null=True,
4243
)
4344

44-
def create(self, validated_data):
45-
created_schedule = super().create(validated_data)
46-
# for iCal-based schedules we need to refresh final schedule information
47-
refresh_ical_final_schedule.apply_async((created_schedule.pk,))
48-
return created_schedule
49-
5045
class Meta:
5146
model = OnCallScheduleICal
5247
fields = [
@@ -60,6 +55,12 @@ class Meta:
6055
"ical_url_overrides": {"required": False, "allow_null": True},
6156
}
6257

58+
def create(self, validated_data):
59+
created_schedule = super().create(validated_data)
60+
# for iCal-based schedules we need to refresh final schedule information
61+
refresh_ical_final_schedule.apply_async((created_schedule.pk,))
62+
return created_schedule
63+
6364

6465
class ScheduleICalUpdateSerializer(ScheduleICalCreateSerializer):
6566
class Meta:

engine/apps/api/serializers/schedule_polymorphic.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
class PolymorphicScheduleSerializer(EagerLoadingMixin, PolymorphicSerializer):
15-
SELECT_RELATED = ["organization", "user_group", "team"]
15+
SELECT_RELATED = ["organization", "team", "user_group", "slack_channel"]
1616

1717
resource_type_field_name = "type"
1818

engine/apps/api/serializers/schedule_web.py

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class ScheduleWebCreateSerializer(ScheduleWebSerializer):
2222
queryset=SlackChannel.objects,
2323
required=False,
2424
allow_null=True,
25+
write_only=True,
2526
)
2627
user_group = OrganizationFilteredPrimaryKeyRelatedField(
2728
filter_field="slack_team_identity__organizations",

engine/apps/api/serializers/slack_channel.py

+8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import typing
2+
13
from rest_framework import serializers
24

35
from apps.slack.models import SlackChannel
46

57

8+
class SlackChannelDetails(typing.TypedDict):
9+
display_name: str
10+
slack_id: str
11+
id: str
12+
13+
614
class SlackChannelSerializer(serializers.ModelSerializer):
715
id = serializers.CharField(read_only=True, source="public_primary_key")
816
display_name = serializers.CharField(source="name")

engine/apps/api/tests/test_channel_filter.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ def test_channel_filter_with_slack_channel_crud(
715715
reverse("api-internal:channel_filter-list"),
716716
data={
717717
"alert_receive_channel": alert_receive_channel.public_primary_key,
718-
"slack_channel_id": slack_channel1.slack_id,
718+
"slack_channel": slack_channel1.slack_id,
719719
},
720720
format="json",
721721
**auth_headers,
@@ -732,7 +732,7 @@ def test_channel_filter_with_slack_channel_crud(
732732
# update the slack channel
733733
url = reverse("api-internal:channel_filter-detail", kwargs={"pk": created_channel_filter["id"]})
734734

735-
response = client.patch(url, data={"slack_channel_id": slack_channel2.slack_id}, format="json", **auth_headers)
735+
response = client.patch(url, data={"slack_channel": slack_channel2.slack_id}, format="json", **auth_headers)
736736

737737
assert response.status_code == status.HTTP_200_OK
738738
assert response.json()["slack_channel"] == {
@@ -742,7 +742,7 @@ def test_channel_filter_with_slack_channel_crud(
742742
}
743743

744744
# remove the slack channel
745-
response = client.patch(url, data={"slack_channel_id": None}, format="json", **auth_headers)
745+
response = client.patch(url, data={"slack_channel": None}, format="json", **auth_headers)
746746

747747
assert response.status_code == status.HTTP_200_OK
748748
assert response.json()["slack_channel"] is None

engine/apps/api/tests/test_schedules.py

+84-3
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,6 @@ def test_create_calendar_schedule(schedule_internal_api_setup, make_user_auth_he
640640
"type": 0,
641641
"name": "created_calendar_schedule",
642642
"time_zone": "UTC",
643-
"slack_channel_id": None,
644643
"user_group": None,
645644
"team": None,
646645
"warnings": [],
@@ -671,7 +670,6 @@ def test_create_ical_schedule(schedule_internal_api_setup, make_user_auth_header
671670
"ical_url_overrides": None,
672671
"name": "created_ical_schedule",
673672
"type": 1,
674-
"slack_channel_id": None,
675673
"user_group": None,
676674
"team": None,
677675
"warnings": [],
@@ -706,7 +704,6 @@ def test_create_web_schedule(schedule_internal_api_setup, make_user_auth_headers
706704
"name": "created_web_schedule",
707705
"type": 2,
708706
"time_zone": "UTC",
709-
"slack_channel_id": None,
710707
"user_group": None,
711708
"team": None,
712709
"warnings": [],
@@ -2459,6 +2456,90 @@ def test_team_not_updated_if_not_in_data(
24592456
assert schedule.team == team
24602457

24612458

2459+
# we don't need to validate the ical URL when creating an ical schedule.. so just patch that functionality
2460+
@patch("apps.api.serializers.schedule_ical.ScheduleICalSerializer.validate_ical_url_primary", return_value=ICAL_URL)
2461+
@pytest.mark.parametrize(
2462+
"schedule_type,other_create_data",
2463+
[
2464+
(0, {}),
2465+
(1, {"ical_url_primary": ICAL_URL}),
2466+
(2, {}),
2467+
],
2468+
)
2469+
@pytest.mark.django_db
2470+
def test_can_update_slack_channel(
2471+
_mock_validate_ical_url_primary,
2472+
make_organization_and_user_with_plugin_token,
2473+
make_slack_team_identity,
2474+
make_slack_channel,
2475+
make_user_auth_headers,
2476+
schedule_type,
2477+
other_create_data,
2478+
):
2479+
organization, user, token = make_organization_and_user_with_plugin_token()
2480+
auth_headers = make_user_auth_headers(user, token)
2481+
slack_team_identity = make_slack_team_identity()
2482+
organization.slack_team_identity = slack_team_identity
2483+
organization.save()
2484+
2485+
slack_channel1 = make_slack_channel(slack_team_identity)
2486+
slack_channel2 = make_slack_channel(slack_team_identity)
2487+
2488+
client = APIClient()
2489+
2490+
# we can set it when creating
2491+
response = client.post(
2492+
reverse("api-internal:schedule-list"),
2493+
{
2494+
"name": "created_schedule",
2495+
"type": schedule_type,
2496+
"slack_channel_id": slack_channel1.public_primary_key,
2497+
**other_create_data,
2498+
},
2499+
format="json",
2500+
**auth_headers,
2501+
)
2502+
2503+
assert response.status_code == status.HTTP_201_CREATED
2504+
2505+
response_data = response.json()
2506+
schedule_id = response_data["id"]
2507+
url = reverse("api-internal:schedule-detail", kwargs={"pk": schedule_id})
2508+
2509+
# NOTE: the response returned by the POST/PUT endpoint currently doesn't include slack_channel_id
2510+
# as it's not used by the UI.. additionally, there was already a bug in it that despite specifying it, it
2511+
# would return null.. the proper way to refactor this is to change the name of slack_channel_id used in the
2512+
# request (as this clashes with the slack_channel_id db column)
2513+
def _assert_slack_channel_updated(new_slack_channel):
2514+
response = client.get(url, **auth_headers)
2515+
2516+
assert response.status_code == status.HTTP_200_OK
2517+
assert response.json()["slack_channel"] == new_slack_channel
2518+
2519+
# we can update it
2520+
response = client.patch(
2521+
url,
2522+
data={
2523+
"slack_channel_id": slack_channel2.public_primary_key,
2524+
},
2525+
format="json",
2526+
**auth_headers,
2527+
)
2528+
assert response.status_code == status.HTTP_200_OK
2529+
_assert_slack_channel_updated(
2530+
{
2531+
"id": slack_channel2.public_primary_key,
2532+
"display_name": slack_channel2.name,
2533+
"slack_id": slack_channel2.slack_id,
2534+
}
2535+
)
2536+
2537+
# we can unset it
2538+
response = client.patch(url, data={"slack_channel_id": None}, format="json", **auth_headers)
2539+
assert response.status_code == status.HTTP_200_OK
2540+
_assert_slack_channel_updated(None)
2541+
2542+
24622543
@patch.object(SlackUserGroup, "can_be_updated", new_callable=PropertyMock)
24632544
@pytest.mark.django_db
24642545
def test_can_update_user_groups(

engine/apps/api/tests/test_set_org_default_slack_channel.py

+51
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,54 @@ def test_set_org_default_slack_channel_permissions(
3636
response = client.post(url, format="json", **make_user_auth_headers(user, token))
3737

3838
assert response.status_code == expected_status
39+
40+
41+
@pytest.mark.django_db
42+
def test_set_organization_slack_default_channel(
43+
make_organization_and_user_with_plugin_token,
44+
make_slack_team_identity,
45+
make_slack_channel,
46+
make_user_auth_headers,
47+
):
48+
slack_team_identity = make_slack_team_identity()
49+
slack_channel = make_slack_channel(slack_team_identity)
50+
51+
organization, user, token = make_organization_and_user_with_plugin_token()
52+
organization.slack_team_identity = slack_team_identity
53+
organization.save()
54+
55+
auth_headers = make_user_auth_headers(user, token)
56+
57+
assert organization.default_slack_channel is None
58+
59+
client = APIClient()
60+
61+
def _update_default_slack_channel(slack_channel_id):
62+
# this endpoint doesn't return any data..
63+
response = client.post(
64+
reverse("api-internal:set-default-slack-channel"),
65+
data={
66+
"id": slack_channel_id,
67+
},
68+
format="json",
69+
**auth_headers,
70+
)
71+
assert response.status_code == status.HTTP_200_OK
72+
73+
def _assert_default_slack_channel_is_updated(slack_channel_id):
74+
response = client.get(reverse("api-internal:api-organization"), format="json", **auth_headers)
75+
assert response.status_code == status.HTTP_200_OK
76+
assert response.json()["slack_channel"] == slack_channel_id
77+
78+
_update_default_slack_channel(slack_channel.public_primary_key)
79+
_assert_default_slack_channel_is_updated(
80+
{
81+
"id": slack_channel.public_primary_key,
82+
"display_name": slack_channel.name,
83+
"slack_id": slack_channel.slack_id,
84+
}
85+
)
86+
87+
# NOTE: currently the endpoint doesn't allow to remove default slack channel, if and when it does, uncomment this
88+
# _update_default_slack_channel(None)
89+
# _assert_default_slack_channel_is_updated(None)

0 commit comments

Comments
 (0)