Add oncall shift endpoint to internal api

This commit is contained in:
Julia 2022-07-12 13:59:17 +03:00
parent c7407d2b39
commit d8a3206130
5 changed files with 312 additions and 7 deletions

View file

@ -0,0 +1,199 @@
from rest_framework import serializers
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb
from apps.user_management.models import User
from common.api_helpers.custom_fields import (
OrganizationFilteredPrimaryKeyRelatedField,
RollingUsersField,
UsersFilteredByOrganizationField,
)
from common.api_helpers.mixins import EagerLoadingMixin
from common.api_helpers.utils import CurrentOrganizationDefault
class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
id = serializers.CharField(read_only=True, source="public_primary_key")
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
type = serializers.ChoiceField(
required=True,
choices=CustomOnCallShift.WEB_TYPES,
)
schedule = OrganizationFilteredPrimaryKeyRelatedField(queryset=OnCallScheduleWeb.objects)
frequency = serializers.ChoiceField(required=False, choices=CustomOnCallShift.FREQUENCY_CHOICES, allow_null=True)
shift_start = serializers.DateTimeField(source="start")
shift_end = serializers.SerializerMethodField()
by_day = serializers.ListField(required=False, allow_null=True)
rolling_users = RollingUsersField(
allow_null=True,
required=False,
child=UsersFilteredByOrganizationField(
queryset=User.objects, required=False, allow_null=True
), # todo: filter by team?
)
class Meta:
model = CustomOnCallShift
fields = [
"id",
"organization",
"name",
"type",
"schedule",
"priority_level",
"shift_start",
"shift_end",
"rotation_start",
"until",
"frequency",
"interval",
"until",
"by_day",
"source",
"rolling_users",
]
extra_kwargs = {
"interval": {"required": False, "allow_null": True},
"source": {"required": False, "write_only": True},
}
SELECT_RELATED = ["schedule"]
def get_shift_end(self, obj):
return obj.start + obj.duration
def to_internal_value(self, data):
data["source"] = CustomOnCallShift.SOURCE_WEB
data["week_start"] = CustomOnCallShift.MONDAY
if not data.get("shift_end"):
raise serializers.ValidationError({"shift_end": ["This field is required."]})
result = super().to_internal_value(data)
return result
def to_representation(self, instance):
result = super().to_representation(instance)
return result
def validate_name(self, name): # todo
organization = self.context["request"].auth.organization
if name is None:
return name
try:
obj = CustomOnCallShift.objects.get(organization=organization, name=name)
except CustomOnCallShift.DoesNotExist:
return name
if self.instance and obj.id == self.instance.id:
return name
else:
raise serializers.ValidationError(["On-call shift with this name already exists"])
def validate_by_day(self, by_day):
if by_day:
for day in by_day:
if day not in CustomOnCallShift.ICAL_WEEKDAY_MAP.keys():
raise serializers.ValidationError(["Invalid day value."])
return by_day
def validate_interval(self, interval):
if interval is not None:
if not isinstance(interval, int) or interval <= 0:
raise serializers.ValidationError(["Invalid value"])
return interval
def validate_rolling_users(self, rolling_users):
result = []
if rolling_users:
for users in rolling_users:
users_dict = dict()
for user in users:
users_dict[user.pk] = user.public_primary_key
result.append(users_dict)
return result
def _validate_shift_end(self, start, end):
if end <= start:
raise serializers.ValidationError({"shift_end": ["Incorrect shift end date"]})
def _validate_frequency(self, frequency, event_type, rolling_users, interval, by_day):
if frequency is None:
if rolling_users and len(rolling_users) > 1:
raise serializers.ValidationError(
{"rolling_users": ["Cannot set multiple user groups for non-recurrent shifts"]}
)
if interval is not None:
raise serializers.ValidationError({"interval": ["Cannot set interval for non-recurrent shifts"]})
if by_day:
raise serializers.ValidationError({"by_day": ["Cannot set days value for non-recurrent shifts"]})
else:
if event_type == CustomOnCallShift.TYPE_OVERRIDE:
raise serializers.ValidationError(
{"frequency": ["Cannot set 'frequency' for shifts with type 'override'"]}
)
if frequency != CustomOnCallShift.FREQUENCY_WEEKLY and by_day:
raise serializers.ValidationError({"by_day": ["Cannot set days value for this frequency type"]})
def _validate_rotations_start(self, shift_start, rotation_start):
if rotation_start < shift_start:
raise serializers.ValidationError({"rotation_start": ["Incorrect rotation start date"]})
def _validate_until(self, rotation_start, until):
if until is not None and until < rotation_start:
raise serializers.ValidationError({"until": ["Incorrect rotation end date"]})
def _correct_validated_data(self, event_type, validated_data):
fields_to_update_for_overrides = [
"priority_level",
"frequency",
"interval",
"by_day",
"until",
]
if event_type == CustomOnCallShift.TYPE_OVERRIDE:
for field in fields_to_update_for_overrides:
value = None
if field == "priority_level":
value = 0
validated_data[field] = value
self._validate_frequency(
validated_data.get("frequency"),
event_type,
validated_data.get("rolling_users"),
validated_data.get("interval"),
validated_data.get("by_day"),
)
self._validate_rotations_start(validated_data["start"], validated_data["rotation_start"])
self._validate_until(validated_data["rotation_start"], validated_data["until"])
# convert shift_end into internal value and validate
raw_shift_end = self.initial_data["shift_end"]
shift_end = serializers.DateTimeField().to_internal_value(raw_shift_end)
self._validate_shift_end(validated_data["start"], shift_end)
validated_data["duration"] = shift_end - validated_data["start"]
return validated_data
def create(self, validated_data):
validated_data = self._correct_validated_data(validated_data["type"], validated_data)
instance = super().create(validated_data)
instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
return instance
class OnCallShiftUpdateSerializer(OnCallShiftSerializer):
schedule = serializers.CharField(read_only=True, source="schedule.public_primary_key")
type = serializers.ReadOnlyField()
class Meta(OnCallShiftSerializer.Meta):
read_only_fields = ("schedule", "type")
def update(self, instance, validated_data):
validated_data = self._correct_validated_data(instance.type, validated_data)
result = super().update(instance, validated_data)
instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
return result

View file

@ -17,6 +17,7 @@ from .views.gitops import TerraformGitOpsView, TerraformStateView
from .views.integration_heartbeat import IntegrationHeartBeatView
from .views.live_setting import LiveSettingViewSet
from .views.maintenance import MaintenanceAPIView, MaintenanceStartAPIView, MaintenanceStopAPIView
from .views.on_call_shifts import OnCallShiftView
from .views.organization import (
CurrentOrganizationView,
GetChannelVerificationCode,
@ -65,6 +66,7 @@ router.register(r"heartbeats", IntegrationHeartBeatView, basename="integration_h
router.register(r"organization_logs", OrganizationLogRecordView, basename="organization_log")
router.register(r"tokens", PublicApiTokenView, basename="api_token")
router.register(r"live_settings", LiveSettingViewSet, basename="live_settings")
router.register(r"oncall_shifts", OnCallShiftView, basename="oncall_shifts")
if settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED:
router.register(r"device/apns", APNSDeviceAuthorizedViewSet)

View file

@ -0,0 +1,100 @@
from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin
from apps.api.serializers.on_call_shifts import OnCallShiftSerializer, OnCallShiftUpdateSerializer
from apps.auth_token.auth import PluginAuthentication
from apps.schedules.models import CustomOnCallShift
from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log
from common.api_helpers.mixins import PublicPrimaryKeyMixin, UpdateSerializerMixin
from common.api_helpers.paginators import FiftyPageSizePaginator
class OnCallShiftView(PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated, ActionPermission)
action_permissions = {
IsAdmin: MODIFY_ACTIONS,
AnyRole: (*READ_ACTIONS, "details", "frequency_options", "days_options"),
}
model = CustomOnCallShift
serializer_class = OnCallShiftSerializer
update_serializer_class = OnCallShiftUpdateSerializer
pagination_class = FiftyPageSizePaginator
filter_backends = [DjangoFilterBackend]
def get_queryset(self):
schedule_id = self.request.query_params.get("schedule_id", None)
lookup_kwargs = {}
if schedule_id:
lookup_kwargs.update({"schedules__public_primary_key": schedule_id})
queryset = CustomOnCallShift.objects.filter(
Q(**lookup_kwargs),
organization=self.request.auth.organization,
team=self.request.user.current_team,
)
queryset = self.serializer_class.setup_eager_loading(queryset)
return queryset.order_by("schedules")
def perform_create(self, serializer):
serializer.save()
instance = serializer.instance
organization = self.request.auth.organization
user = self.request.user
description = (
f"Custom on-call shift with params: {instance.repr_settings_for_client_side_logging} "
f"was created" # todo
)
create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_CREATED, description)
def perform_update(self, serializer):
organization = self.request.auth.organization
user = self.request.user
old_state = serializer.instance.repr_settings_for_client_side_logging
serializer.save()
new_state = serializer.instance.repr_settings_for_client_side_logging
description = f"Settings of custom on-call shift was changed " f"from:\n{old_state}\nto:\n{new_state}"
create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_CHANGED, description)
def perform_destroy(self, instance):
organization = self.request.auth.organization
user = self.request.user
description = (
f"Custom on-call shift " f"with params: {instance.repr_settings_for_client_side_logging} was deleted"
)
create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_DELETED, description)
instance.delete()
@action(detail=False, methods=["get"])
def frequency_options(self, request):
return Response(
[
{
"display_name": display_name,
"value": freq,
}
for freq, display_name in CustomOnCallShift.WEB_FREQUENCY_CHOICES_MAP.items()
]
)
@action(detail=False, methods=["get"])
def days_options(self, request):
return Response(
[
{
"display_name": display_name,
"value": day_number,
}
for day_number, display_name in CustomOnCallShift.WEEKDAY_CHOICES
]
)

View file

@ -9,18 +9,16 @@ from apps.schedules.tasks import (
schedule_notify_about_gaps_in_schedule,
)
from apps.user_management.models import User
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField, UsersFilteredByOrganizationField
from common.api_helpers.custom_fields import (
RollingUsersField,
TeamPrimaryKeyRelatedField,
UsersFilteredByOrganizationField,
)
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.mixins import EagerLoadingMixin
from common.api_helpers.utils import CurrentOrganizationDefault
class RollingUsersField(serializers.ListField):
def to_representation(self, value):
result = [list(d.values()) for d in value]
return result
class CustomOnCallShiftTypeField(fields.CharField):
def to_representation(self, value):
return CustomOnCallShift.PUBLIC_TYPE_CHOICES_MAP[value]

View file

@ -171,3 +171,9 @@ class UserIdField(fields.CharField):
if value is not None:
return value.public_primary_key
return value
class RollingUsersField(serializers.ListField):
def to_representation(self, value):
result = [list(d.values()) for d in value]
return result