2024-06-03 14:06:47 +01:00
|
|
|
from datetime import timedelta
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
from django.core.exceptions import ObjectDoesNotExist
|
2024-01-12 15:11:22 +00:00
|
|
|
from drf_spectacular.utils import extend_schema_field
|
2022-06-03 08:09:47 -06:00
|
|
|
from rest_framework import fields, serializers
|
|
|
|
|
from rest_framework.exceptions import ValidationError
|
|
|
|
|
from rest_framework.relations import RelatedField
|
|
|
|
|
|
|
|
|
|
from apps.alerts.models import ChannelFilter
|
|
|
|
|
from apps.user_management.models import User
|
|
|
|
|
from common.api_helpers.exceptions import BadRequest
|
2023-07-21 21:35:19 +02:00
|
|
|
from common.timezones import raise_exception_if_not_valid_timezone
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
2024-01-12 15:11:22 +00:00
|
|
|
@extend_schema_field(serializers.CharField)
|
2022-06-03 08:09:47 -06:00
|
|
|
class OrganizationFilteredPrimaryKeyRelatedField(RelatedField):
|
|
|
|
|
"""
|
|
|
|
|
This field is used to filter entities by organization
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|
self.filter_field = kwargs.pop("filter_field", "organization")
|
|
|
|
|
self.display_func = kwargs.pop("display_func", lambda instance: str(instance))
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
|
return value.public_primary_key
|
|
|
|
|
|
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
|
try:
|
|
|
|
|
return self.get_queryset().get(public_primary_key=data)
|
|
|
|
|
except ObjectDoesNotExist:
|
|
|
|
|
raise ValidationError("Object does not exist")
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
raise ValidationError("Invalid values")
|
|
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
|
request = self.context.get("request", None)
|
|
|
|
|
queryset = self.queryset
|
|
|
|
|
if not request or not queryset:
|
|
|
|
|
return None
|
|
|
|
|
filter_kwargs = {self.filter_field: request.auth.organization}
|
|
|
|
|
return queryset.filter(**filter_kwargs).distinct()
|
|
|
|
|
|
|
|
|
|
def display_value(self, instance):
|
|
|
|
|
return self.display_func(instance)
|
|
|
|
|
|
|
|
|
|
|
2024-01-12 15:11:22 +00:00
|
|
|
@extend_schema_field(serializers.CharField)
|
2022-06-03 08:09:47 -06:00
|
|
|
class TeamPrimaryKeyRelatedField(RelatedField):
|
|
|
|
|
"""
|
|
|
|
|
This field is used to get user teams
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|
self.display_func = kwargs.pop("display_func", lambda instance: str(instance))
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
|
return value.public_primary_key
|
|
|
|
|
|
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
|
try:
|
|
|
|
|
return self.get_queryset().get(public_primary_key=data)
|
|
|
|
|
except ObjectDoesNotExist:
|
|
|
|
|
raise ValidationError("Object does not exist")
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
raise ValidationError("Invalid values")
|
|
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
|
request = self.context.get("request", None)
|
|
|
|
|
if not request:
|
|
|
|
|
return None
|
2023-03-22 00:57:20 +08:00
|
|
|
return request.user.available_teams.all()
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
def display_value(self, instance):
|
|
|
|
|
return self.display_func(instance)
|
|
|
|
|
|
2023-03-22 00:57:20 +08:00
|
|
|
def validate_empty_values(self, data):
|
|
|
|
|
if data == "null":
|
|
|
|
|
data = None
|
|
|
|
|
return super().validate_empty_values(data)
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2024-10-24 11:24:36 +02:00
|
|
|
@extend_schema_field(serializers.ListField(child=serializers.CharField()))
|
2022-06-03 08:09:47 -06:00
|
|
|
class UsersFilteredByOrganizationField(serializers.Field):
|
|
|
|
|
"""
|
|
|
|
|
This field reduces queries count when accessing User many related field (ex: notify_to_users_queue).
|
|
|
|
|
Check if you can use OrganizationFilteredPrimaryKeyRelatedField before using this one.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|
self.queryset = kwargs.pop("queryset", None)
|
2024-09-21 02:36:33 +05:30
|
|
|
self.require_all_exist = kwargs.pop("require_all_exist", False)
|
2022-06-03 08:09:47 -06:00
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
|
return list(map(lambda v: v.public_primary_key, value.all()))
|
|
|
|
|
|
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
|
queryset = self.queryset
|
|
|
|
|
request = self.context.get("request", None)
|
|
|
|
|
|
|
|
|
|
if not request or not queryset:
|
|
|
|
|
return None
|
|
|
|
|
|
2024-09-21 02:36:33 +05:30
|
|
|
users = queryset.filter(organization=request.user.organization, public_primary_key__in=data).distinct()
|
|
|
|
|
users_ppk = set(u.public_primary_key for u in users)
|
|
|
|
|
data_set = set(data)
|
|
|
|
|
|
|
|
|
|
if not self.require_all_exist:
|
|
|
|
|
return users
|
|
|
|
|
|
|
|
|
|
if len(data_set) != len(users_ppk):
|
|
|
|
|
missing_users = data_set - users_ppk
|
|
|
|
|
raise ValidationError(f"User does not exist {missing_users}")
|
|
|
|
|
|
|
|
|
|
return users
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
feat: convert `schedule.channel` (char field) to `schedule.slack_channel` (foreign key) (#5199)
# What this PR does
`OnCallSchedule` equivalent of
https://github.com/grafana/oncall/pull/5191.
**NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so
that I can use some of the new serializer fields defined in there)
### Migration
```bash
Running migrations: │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1 │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7). │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0. │
│ Applying schedules.0019_auto_20241021_1735... OK
```
### Tested Public API
```txt
POST {{oncall_host}}/api/v1/schedules/
Authorization: {{oncall_api_key}}
Content-Type: application/json
{
"name": "Demo testy testy2",
"type": "web",
"time_zone": "America/Los_Angeles",
"slack": {
"channel_id": "C05PPLYN1U1"
}
}
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Accept, Origin
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 198
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{
"id": "SBBN73UTUTVCE",
"team_id": null,
"name": "Demo testy testy2",
"time_zone": "America/Los_Angeles",
"on_call_now": [],
"shifts": [],
"slack": {
"channel_id": "C05PPLYN1U1",
"user_group_id": null
},
"type": "web"
}
```
### Tested via UI (eg; internal API)
https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af
## 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.
2024-11-04 14:27:21 -05:00
|
|
|
# TODO: update the following once we bump mypy to 1.11 (which supports generics)
|
|
|
|
|
# class _SlackObjectFilteredByOrganizationSlackWorkspaceField[O: ("SlackChannel", "SlackUserGroup")](RelatedField[O]):
|
|
|
|
|
class _SlackObjectFilteredByOrganizationSlackWorkspaceField(RelatedField):
|
|
|
|
|
@property
|
|
|
|
|
def slack_team_identity_field(self):
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def slack_object_singular_noun(self):
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2024-11-04 13:34:06 -05:00
|
|
|
def get_queryset(self):
|
|
|
|
|
request = self.context.get("request", None)
|
|
|
|
|
if not request:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
organization = request.user.organization
|
|
|
|
|
if organization.slack_team_identity is None:
|
|
|
|
|
raise BadRequest(detail="Slack isn't connected to this workspace")
|
|
|
|
|
|
feat: convert `schedule.channel` (char field) to `schedule.slack_channel` (foreign key) (#5199)
# What this PR does
`OnCallSchedule` equivalent of
https://github.com/grafana/oncall/pull/5191.
**NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so
that I can use some of the new serializer fields defined in there)
### Migration
```bash
Running migrations: │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1 │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7). │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0. │
│ Applying schedules.0019_auto_20241021_1735... OK
```
### Tested Public API
```txt
POST {{oncall_host}}/api/v1/schedules/
Authorization: {{oncall_api_key}}
Content-Type: application/json
{
"name": "Demo testy testy2",
"type": "web",
"time_zone": "America/Los_Angeles",
"slack": {
"channel_id": "C05PPLYN1U1"
}
}
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Accept, Origin
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 198
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{
"id": "SBBN73UTUTVCE",
"team_id": null,
"name": "Demo testy testy2",
"time_zone": "America/Los_Angeles",
"on_call_now": [],
"shifts": [],
"slack": {
"channel_id": "C05PPLYN1U1",
"user_group_id": null
},
"type": "web"
}
```
### Tested via UI (eg; internal API)
https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af
## 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.
2024-11-04 14:27:21 -05:00
|
|
|
slack_team_identity_related_objects = getattr(organization.slack_team_identity, self.slack_team_identity_field)
|
|
|
|
|
return slack_team_identity_related_objects.all()
|
2024-11-04 13:34:06 -05:00
|
|
|
|
|
|
|
|
def to_internal_value(self, slack_id: str):
|
feat: convert `schedule.channel` (char field) to `schedule.slack_channel` (foreign key) (#5199)
# What this PR does
`OnCallSchedule` equivalent of
https://github.com/grafana/oncall/pull/5191.
**NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so
that I can use some of the new serializer fields defined in there)
### Migration
```bash
Running migrations: │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1 │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7). │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0. │
│ Applying schedules.0019_auto_20241021_1735... OK
```
### Tested Public API
```txt
POST {{oncall_host}}/api/v1/schedules/
Authorization: {{oncall_api_key}}
Content-Type: application/json
{
"name": "Demo testy testy2",
"type": "web",
"time_zone": "America/Los_Angeles",
"slack": {
"channel_id": "C05PPLYN1U1"
}
}
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Accept, Origin
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 198
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{
"id": "SBBN73UTUTVCE",
"team_id": null,
"name": "Demo testy testy2",
"time_zone": "America/Los_Angeles",
"on_call_now": [],
"shifts": [],
"slack": {
"channel_id": "C05PPLYN1U1",
"user_group_id": null
},
"type": "web"
}
```
### Tested via UI (eg; internal API)
https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af
## 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.
2024-11-04 14:27:21 -05:00
|
|
|
noun = self.slack_object_singular_noun
|
|
|
|
|
|
2024-11-04 13:34:06 -05:00
|
|
|
try:
|
|
|
|
|
return self.get_queryset().get(slack_id=slack_id.upper())
|
|
|
|
|
except ObjectDoesNotExist:
|
feat: convert `schedule.channel` (char field) to `schedule.slack_channel` (foreign key) (#5199)
# What this PR does
`OnCallSchedule` equivalent of
https://github.com/grafana/oncall/pull/5191.
**NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so
that I can use some of the new serializer fields defined in there)
### Migration
```bash
Running migrations: │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1 │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7). │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0. │
│ Applying schedules.0019_auto_20241021_1735... OK
```
### Tested Public API
```txt
POST {{oncall_host}}/api/v1/schedules/
Authorization: {{oncall_api_key}}
Content-Type: application/json
{
"name": "Demo testy testy2",
"type": "web",
"time_zone": "America/Los_Angeles",
"slack": {
"channel_id": "C05PPLYN1U1"
}
}
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Accept, Origin
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 198
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{
"id": "SBBN73UTUTVCE",
"team_id": null,
"name": "Demo testy testy2",
"time_zone": "America/Los_Angeles",
"on_call_now": [],
"shifts": [],
"slack": {
"channel_id": "C05PPLYN1U1",
"user_group_id": null
},
"type": "web"
}
```
### Tested via UI (eg; internal API)
https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af
## 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.
2024-11-04 14:27:21 -05:00
|
|
|
raise ValidationError(f"Slack {noun} does not exist")
|
2024-11-04 13:34:06 -05:00
|
|
|
except (TypeError, ValueError, AttributeError):
|
feat: convert `schedule.channel` (char field) to `schedule.slack_channel` (foreign key) (#5199)
# What this PR does
`OnCallSchedule` equivalent of
https://github.com/grafana/oncall/pull/5191.
**NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so
that I can use some of the new serializer fields defined in there)
### Migration
```bash
Running migrations: │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1 │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7). │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0. │
│ Applying schedules.0019_auto_20241021_1735... OK
```
### Tested Public API
```txt
POST {{oncall_host}}/api/v1/schedules/
Authorization: {{oncall_api_key}}
Content-Type: application/json
{
"name": "Demo testy testy2",
"type": "web",
"time_zone": "America/Los_Angeles",
"slack": {
"channel_id": "C05PPLYN1U1"
}
}
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Accept, Origin
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 198
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{
"id": "SBBN73UTUTVCE",
"team_id": null,
"name": "Demo testy testy2",
"time_zone": "America/Los_Angeles",
"on_call_now": [],
"shifts": [],
"slack": {
"channel_id": "C05PPLYN1U1",
"user_group_id": null
},
"type": "web"
}
```
### Tested via UI (eg; internal API)
https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af
## 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.
2024-11-04 14:27:21 -05:00
|
|
|
raise ValidationError(f"Invalid Slack {noun}")
|
2024-11-04 13:34:06 -05:00
|
|
|
|
feat: convert `schedule.channel` (char field) to `schedule.slack_channel` (foreign key) (#5199)
# What this PR does
`OnCallSchedule` equivalent of
https://github.com/grafana/oncall/pull/5191.
**NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so
that I can use some of the new serializer fields defined in there)
### Migration
```bash
Running migrations: │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1 │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7). │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0. │
│ Applying schedules.0019_auto_20241021_1735... OK
```
### Tested Public API
```txt
POST {{oncall_host}}/api/v1/schedules/
Authorization: {{oncall_api_key}}
Content-Type: application/json
{
"name": "Demo testy testy2",
"type": "web",
"time_zone": "America/Los_Angeles",
"slack": {
"channel_id": "C05PPLYN1U1"
}
}
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Accept, Origin
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 198
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{
"id": "SBBN73UTUTVCE",
"team_id": null,
"name": "Demo testy testy2",
"time_zone": "America/Los_Angeles",
"on_call_now": [],
"shifts": [],
"slack": {
"channel_id": "C05PPLYN1U1",
"user_group_id": null
},
"type": "web"
}
```
### Tested via UI (eg; internal API)
https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af
## 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.
2024-11-04 14:27:21 -05:00
|
|
|
def to_representation(self, obj) -> str:
|
2024-11-04 13:34:06 -05:00
|
|
|
return obj.public_primary_key
|
|
|
|
|
|
|
|
|
|
|
feat: convert `schedule.channel` (char field) to `schedule.slack_channel` (foreign key) (#5199)
# What this PR does
`OnCallSchedule` equivalent of
https://github.com/grafana/oncall/pull/5191.
**NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so
that I can use some of the new serializer fields defined in there)
### Migration
```bash
Running migrations: │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1 │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7). │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel. │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0. │
│ Applying schedules.0019_auto_20241021_1735... OK
```
### Tested Public API
```txt
POST {{oncall_host}}/api/v1/schedules/
Authorization: {{oncall_api_key}}
Content-Type: application/json
{
"name": "Demo testy testy2",
"type": "web",
"time_zone": "America/Los_Angeles",
"slack": {
"channel_id": "C05PPLYN1U1"
}
}
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Accept, Origin
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 198
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{
"id": "SBBN73UTUTVCE",
"team_id": null,
"name": "Demo testy testy2",
"time_zone": "America/Los_Angeles",
"on_call_now": [],
"shifts": [],
"slack": {
"channel_id": "C05PPLYN1U1",
"user_group_id": null
},
"type": "web"
}
```
### Tested via UI (eg; internal API)
https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af
## 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.
2024-11-04 14:27:21 -05:00
|
|
|
# TODO: update the following once we bump mypy to 1.11 (which supports generics)
|
|
|
|
|
# class SlackChannelsFilteredByOrganizationSlackWorkspaceField(
|
|
|
|
|
# _SlackObjectFilteredByOrganizationSlackWorkspaceField["SlackChannel"],
|
|
|
|
|
# ):
|
|
|
|
|
class SlackChannelsFilteredByOrganizationSlackWorkspaceField(_SlackObjectFilteredByOrganizationSlackWorkspaceField):
|
|
|
|
|
@property
|
|
|
|
|
def slack_team_identity_field(self):
|
|
|
|
|
return "cached_channels"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def slack_object_singular_noun(self):
|
|
|
|
|
return "channel"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: update the following once we bump mypy to 1.11 (which supports generics)
|
|
|
|
|
# class SlackUserGroupsFilteredByOrganizationSlackWorkspaceField(
|
|
|
|
|
# _SlackObjectFilteredByOrganizationSlackWorkspaceField["SlackUserGroup"],
|
|
|
|
|
# ):
|
|
|
|
|
class SlackUserGroupsFilteredByOrganizationSlackWorkspaceField(_SlackObjectFilteredByOrganizationSlackWorkspaceField):
|
|
|
|
|
@property
|
|
|
|
|
def slack_team_identity_field(self):
|
|
|
|
|
return "usergroups"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def slack_object_singular_noun(self):
|
|
|
|
|
return "user group"
|
|
|
|
|
|
|
|
|
|
|
2024-02-23 08:55:44 -03:00
|
|
|
class IntegrationFilteredByOrganizationField(serializers.RelatedField):
|
|
|
|
|
def get_queryset(self):
|
|
|
|
|
request = self.context.get("request", None)
|
|
|
|
|
if not request:
|
|
|
|
|
return None
|
|
|
|
|
return request.user.organization.alert_receive_channels.all()
|
|
|
|
|
|
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
|
try:
|
|
|
|
|
return self.get_queryset().get(public_primary_key=data)
|
|
|
|
|
except ObjectDoesNotExist:
|
|
|
|
|
raise ValidationError("Integration does not exist")
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
raise ValidationError("Invalid integration")
|
|
|
|
|
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
|
return value.public_primary_key
|
|
|
|
|
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
class RouteIdField(fields.CharField):
|
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
|
try:
|
|
|
|
|
channel_filter = ChannelFilter.objects.get(public_primary_key=data)
|
|
|
|
|
except ChannelFilter.DoesNotExist:
|
|
|
|
|
raise BadRequest(detail="Route does not exist")
|
|
|
|
|
return channel_filter
|
|
|
|
|
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
|
if value is not None:
|
|
|
|
|
return value.public_primary_key
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UserIdField(fields.CharField):
|
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
|
request = self.context.get("request", None)
|
|
|
|
|
|
|
|
|
|
user = User.objects.filter(organization=request.auth.organization, public_primary_key=data).first()
|
|
|
|
|
if user is None:
|
|
|
|
|
raise BadRequest(detail="User does not exist")
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
|
if value is not None:
|
|
|
|
|
return value.public_primary_key
|
|
|
|
|
return value
|
2022-07-12 13:59:17 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class RollingUsersField(serializers.ListField):
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
|
result = [list(d.values()) for d in value]
|
|
|
|
|
return result
|
2023-07-21 21:35:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TimeZoneField(serializers.CharField):
|
|
|
|
|
def _validator(self, value: str):
|
|
|
|
|
raise_exception_if_not_valid_timezone(value, serializers.ValidationError)
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|
super().__init__(validators=[self._validator], **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TimeZoneAwareDatetimeField(serializers.DateTimeField):
|
|
|
|
|
"""
|
|
|
|
|
This serializer field ensures that datetimes are always
|
|
|
|
|
passed in ISO-8601 format (https://en.wikipedia.org/wiki/ISO_8601) with one caveat, timezone information MUST
|
|
|
|
|
be passed in. ISO-8601 allows timezone information to be optional.
|
|
|
|
|
|
|
|
|
|
All of the following would be considered valid datetimes by this field:
|
|
|
|
|
2023-07-20T18:35:19+00:00
|
|
|
|
|
2023-07-20T18:35:19Z
|
|
|
|
|
|
|
|
|
|
These are not valid:
|
|
|
|
|
2023-07-20 12:00:00
|
|
|
|
|
20230720T120000Z
|
|
|
|
|
|
|
|
|
|
This allows us to capture timezone information at insert/update time. Django converts/persists this information
|
|
|
|
|
in UTC, and then when it is read back, you can be 100% sure that you are working with a UTC timezone aware datetime.
|
|
|
|
|
|
|
|
|
|
Additionally, it standardizes how we format returned datetime strings.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
UTC_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
|
|
|
|
UTC_FORMAT_WITH_MICROSECONDS = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
|
|
|
|
|
|
|
|
UTC_OFFSET_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
|
|
|
|
UTC_OFFSET_FORMAT_WITH_MICROSECONDS = "%Y-%m-%dT%H:%M:%S.%f%z"
|
|
|
|
|
"`%z` = UTC offset in the form +HHMM or -HHMM. (a colon separator can optionally be included)"
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|
# we could use 'iso-8601' as a valid value to input_formats, however, see the note above about it
|
|
|
|
|
# allowing timezone naive datetimes
|
|
|
|
|
super().__init__(
|
|
|
|
|
format=self.UTC_FORMAT_WITH_MICROSECONDS,
|
|
|
|
|
input_formats=[
|
|
|
|
|
self.UTC_FORMAT,
|
|
|
|
|
self.UTC_FORMAT_WITH_MICROSECONDS,
|
|
|
|
|
self.UTC_OFFSET_FORMAT,
|
|
|
|
|
self.UTC_OFFSET_FORMAT_WITH_MICROSECONDS,
|
|
|
|
|
],
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
2024-06-03 14:06:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class DurationSecondsField(serializers.FloatField):
|
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
|
return timedelta(seconds=int(super().to_internal_value(data)))
|
|
|
|
|
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
|
return str(value.total_seconds())
|