From c7407d2b390435695d9d3147b028af72be811e28 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Jul 2022 13:55:09 +0300 Subject: [PATCH 01/12] Add `rotation_start` for oncall shifts, update model with web choices --- .../0006_customoncallshift_rotation_start.py | 35 +++++++++++++++++++ .../schedules/models/custom_on_call_shift.py | 30 ++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 engine/apps/schedules/migrations/0006_customoncallshift_rotation_start.py diff --git a/engine/apps/schedules/migrations/0006_customoncallshift_rotation_start.py b/engine/apps/schedules/migrations/0006_customoncallshift_rotation_start.py new file mode 100644 index 00000000..9facd79b --- /dev/null +++ b/engine/apps/schedules/migrations/0006_customoncallshift_rotation_start.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.13 on 2022-07-12 08:03 + +from django.db import migrations, models + + +def fill_rotation_start_field(apps, schema_editor): + CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift") + shifts = CustomOnCallShift.objects.all() + shifts_to_update = [] + for shift in shifts: + shift.rotation_start = shift.start + shifts_to_update.append(shift) + + CustomOnCallShift.objects.bulk_update(shifts_to_update, ["rotation_start"], batch_size=5000) + + +class Migration(migrations.Migration): + + dependencies = [ + ('schedules', '0005_auto_20220704_1947'), + ] + + operations = [ + migrations.AddField( + model_name='customoncallshift', + name='rotation_start', + field=models.DateTimeField(default=None, null=True), + ), + migrations.RunPython(fill_rotation_start_field, migrations.RunPython.noop), + migrations.AlterField( + model_name='customoncallshift', + name='rotation_start', + field=models.DateTimeField(), + ), + ] diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 0f2753da..442f88a7 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -13,7 +13,11 @@ from django.utils.functional import cached_property from icalendar.cal import Event from recurring_ical_events import UnfoldableCalendar -from apps.schedules.tasks import drop_cached_ical_task +from apps.schedules.tasks import ( + drop_cached_ical_task, + schedule_notify_about_empty_shifts_in_schedule, + schedule_notify_about_gaps_in_schedule, +) from apps.user_management.models import User from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -57,6 +61,13 @@ class CustomOnCallShift(models.Model): FREQUENCY_MONTHLY: "monthly", } + WEB_FREQUENCY_CHOICES_MAP = { + FREQUENCY_HOURLY: "hours", + FREQUENCY_DAILY: "days", + FREQUENCY_WEEKLY: "weeks", + FREQUENCY_MONTHLY: "months", + } + ( TYPE_SINGLE_EVENT, TYPE_RECURRENT_EVENT, @@ -78,6 +89,11 @@ class CustomOnCallShift(models.Model): TYPE_OVERRIDE: "override", } + WEB_TYPES = ( + TYPE_ROLLING_USERS_EVENT, + TYPE_OVERRIDE, + ) + (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7) WEEKDAY_CHOICES = ( @@ -151,6 +167,8 @@ class CustomOnCallShift(models.Model): start = models.DateTimeField() # event start datetime duration = models.DurationField() # duration in seconds + rotation_start = models.DateTimeField() # used for calculation users rotation and rotation start date + frequency = models.IntegerField(choices=FREQUENCY_CHOICES, null=True, default=None) priority_level = models.IntegerField(default=0) @@ -173,7 +191,10 @@ class CustomOnCallShift(models.Model): def delete(self, *args, **kwargs): for schedule in self.schedules.all(): - drop_cached_ical_task.apply_async((schedule.pk,)) + self.start_drop_ical_and_check_schedule_tasks(schedule) + if self.schedule: + self.start_drop_ical_and_check_schedule_tasks(self.schedule) + # todo: add soft delete super().delete(*args, **kwargs) @property @@ -356,3 +377,8 @@ class CustomOnCallShift(models.Model): result.append({user.pk: user.public_primary_key for user in users}) self.rolling_users = result self.save(update_fields=["rolling_users"]) + + def start_drop_ical_and_check_schedule_tasks(self, schedule): + drop_cached_ical_task.apply_async((schedule.pk,)) + schedule_notify_about_empty_shifts_in_schedule.apply_async((schedule.pk,)) + schedule_notify_about_gaps_in_schedule.apply_async((schedule.pk,)) From d8a32061302f4ee59cdd015ef074f9641abf3899 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Jul 2022 13:59:17 +0300 Subject: [PATCH 02/12] Add oncall shift endpoint to internal api --- engine/apps/api/serializers/on_call_shifts.py | 199 ++++++++++++++++++ engine/apps/api/urls.py | 2 + engine/apps/api/views/on_call_shifts.py | 100 +++++++++ .../public_api/serializers/on_call_shifts.py | 12 +- engine/common/api_helpers/custom_fields.py | 6 + 5 files changed, 312 insertions(+), 7 deletions(-) create mode 100644 engine/apps/api/serializers/on_call_shifts.py create mode 100644 engine/apps/api/views/on_call_shifts.py diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py new file mode 100644 index 00000000..8d789a73 --- /dev/null +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -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 diff --git a/engine/apps/api/urls.py b/engine/apps/api/urls.py index a82ee4e9..1eb9adf2 100644 --- a/engine/apps/api/urls.py +++ b/engine/apps/api/urls.py @@ -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) diff --git a/engine/apps/api/views/on_call_shifts.py b/engine/apps/api/views/on_call_shifts.py new file mode 100644 index 00000000..b4c93449 --- /dev/null +++ b/engine/apps/api/views/on_call_shifts.py @@ -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 + ] + ) diff --git a/engine/apps/public_api/serializers/on_call_shifts.py b/engine/apps/public_api/serializers/on_call_shifts.py index 1fd85aa6..9b87f7f7 100644 --- a/engine/apps/public_api/serializers/on_call_shifts.py +++ b/engine/apps/public_api/serializers/on_call_shifts.py @@ -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] diff --git a/engine/common/api_helpers/custom_fields.py b/engine/common/api_helpers/custom_fields.py index 597aff52..79b96012 100644 --- a/engine/common/api_helpers/custom_fields.py +++ b/engine/common/api_helpers/custom_fields.py @@ -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 From 6dc4657371a5fa6eac798406e9298d93aaf53fa4 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Jul 2022 14:46:34 +0300 Subject: [PATCH 03/12] fix searching shifts by schedule_id --- engine/apps/api/views/on_call_shifts.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/engine/apps/api/views/on_call_shifts.py b/engine/apps/api/views/on_call_shifts.py index b4c93449..2aa8c689 100644 --- a/engine/apps/api/views/on_call_shifts.py +++ b/engine/apps/api/views/on_call_shifts.py @@ -33,12 +33,14 @@ class OnCallShiftView(PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet def get_queryset(self): schedule_id = self.request.query_params.get("schedule_id", None) - lookup_kwargs = {} + lookup_kwargs = Q() if schedule_id: - lookup_kwargs.update({"schedules__public_primary_key": schedule_id}) + lookup_kwargs = Q( + Q(schedule__public_primary_key=schedule_id) | Q(schedules__public_primary_key=schedule_id) + ) queryset = CustomOnCallShift.objects.filter( - Q(**lookup_kwargs), + lookup_kwargs, organization=self.request.auth.organization, team=self.request.user.current_team, ) From 5ebcb3949926547daabf0d5c0e403cd2368d2378 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Jul 2022 17:13:56 +0300 Subject: [PATCH 04/12] fix by_day value, fix getting next event date with respect to rotation end date --- engine/apps/api/serializers/on_call_shifts.py | 2 +- engine/apps/api/views/on_call_shifts.py | 4 ++-- .../schedules/models/custom_on_call_shift.py | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index 8d789a73..4d3739e1 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -90,7 +90,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): def validate_by_day(self, by_day): if by_day: for day in by_day: - if day not in CustomOnCallShift.ICAL_WEEKDAY_MAP.keys(): + if day not in CustomOnCallShift.WEB_WEEKDAY_MAP: raise serializers.ValidationError(["Invalid day value."]) return by_day diff --git a/engine/apps/api/views/on_call_shifts.py b/engine/apps/api/views/on_call_shifts.py index 2aa8c689..a12e5c0b 100644 --- a/engine/apps/api/views/on_call_shifts.py +++ b/engine/apps/api/views/on_call_shifts.py @@ -95,8 +95,8 @@ class OnCallShiftView(PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet [ { "display_name": display_name, - "value": day_number, + "value": value, } - for day_number, display_name in CustomOnCallShift.WEEKDAY_CHOICES + for value, display_name in CustomOnCallShift.WEB_WEEKDAY_MAP.items() ] ) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 442f88a7..15c35da8 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -115,6 +115,16 @@ class CustomOnCallShift(models.Model): SATURDAY: "SA", SUNDAY: "SU", } + + WEB_WEEKDAY_MAP = { + "MO": "Monday", + "TU": "Tuesday", + "WE": "Wednesday", + "TH": "Thursday", + "FR": "Friday", + "SA": "Saturday", + "SU": "Sunday", + } ( SOURCE_WEB, SOURCE_API, @@ -240,6 +250,8 @@ class CustomOnCallShift(models.Model): users_queue = self.get_rolling_users() for counter, users in enumerate(users_queue, start=1): start = self.get_next_start_date(event_ical) + if not start: # means that rotation ends before next event starts + break for user_counter, user in enumerate(users, start=1): event_ical = self.generate_ical(user, start, user_counter, counter, time_zone) result += event_ical @@ -301,6 +313,10 @@ class CustomOnCallShift(models.Model): if days_for_next_event > DAYS_IN_A_MONTH: days_for_next_event = days_for_next_event % DAYS_IN_A_MONTH next_event_start = current_event_start + timezone.timedelta(days=days_for_next_event) + + # check if rotation ends before next event starts + if self.until and next_event_start > self.until: + return next_event = None # repetitions generate the next event shift according with the recurrence rules repetitions = UnfoldableCalendar(current_event).RepeatedEvent( From 1893b784e87cb0c91663b339b01938eb5d5a8e1d Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 18 Jul 2022 12:36:26 +0300 Subject: [PATCH 05/12] Update oncall shifts public api endpoint --- .../apps/public_api/serializers/on_call_shifts.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/engine/apps/public_api/serializers/on_call_shifts.py b/engine/apps/public_api/serializers/on_call_shifts.py index 9b87f7f7..4471c6c0 100644 --- a/engine/apps/public_api/serializers/on_call_shifts.py +++ b/engine/apps/public_api/serializers/on_call_shifts.py @@ -3,11 +3,6 @@ import time from rest_framework import fields, serializers from apps.schedules.models import CustomOnCallShift -from apps.schedules.tasks import ( - drop_cached_ical_task, - schedule_notify_about_empty_shifts_in_schedule, - schedule_notify_about_gaps_in_schedule, -) from apps.user_management.models import User from common.api_helpers.custom_fields import ( RollingUsersField, @@ -136,6 +131,8 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer validated_data.get("by_monthday"), ) instance = super().create(validated_data) + for schedule in instance.schedules.all(): + instance.start_drop_ical_and_check_schedule_tasks(schedule) return instance def validate_name(self, name): @@ -308,6 +305,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer "by_monthday", "rolling_users", "start_rotation_from_user_index", + "until", ], } for field in fields_to_update_map[event_type]: @@ -354,9 +352,5 @@ class CustomOnCallShiftUpdateSerializer(CustomOnCallShiftSerializer): validated_data = self._correct_validated_data(event_type, validated_data) result = super().update(instance, validated_data) for schedule in instance.schedules.all(): - drop_cached_ical_task.apply_async( - (schedule.pk,), - ) - schedule_notify_about_empty_shifts_in_schedule.apply_async((instance.pk,)) - schedule_notify_about_gaps_in_schedule.apply_async((instance.pk,)) + instance.start_drop_ical_and_check_schedule_tasks(schedule) return result From a4c481a1ff74e6565c3e61d9c40a15b412234f48 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 18 Jul 2022 12:59:50 +0300 Subject: [PATCH 06/12] Add `rotation_start` field to shifts public api endpoint, fix tests --- .../tests/test_escalation_policy_snapshot.py | 8 +++-- .../alerts/tests/test_terraform_renderer.py | 1 + engine/apps/api/serializers/on_call_shifts.py | 4 +-- engine/apps/api/tests/test_schedules.py | 6 +++- .../public_api/serializers/on_call_shifts.py | 8 +++++ .../public_api/tests/test_on_call_shifts.py | 32 +++++++++++++++---- .../apps/public_api/tests/test_schedules.py | 12 +++++-- .../apps/schedules/tests/test_ical_utils.py | 1 + 8 files changed, 57 insertions(+), 15 deletions(-) diff --git a/engine/apps/alerts/tests/test_escalation_policy_snapshot.py b/engine/apps/alerts/tests/test_escalation_policy_snapshot.py index 9a555c35..6a4e1630 100644 --- a/engine/apps/alerts/tests/test_escalation_policy_snapshot.py +++ b/engine/apps/alerts/tests/test_escalation_policy_snapshot.py @@ -170,8 +170,10 @@ def test_escalation_step_notify_on_call_schedule( schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) # create on_call_shift with user to notify + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": timezone.datetime.now().replace(microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": timezone.timedelta(seconds=7200), } on_call_shift = make_on_call_shift( @@ -216,8 +218,10 @@ def test_escalation_step_notify_on_call_schedule_viewer_user( schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) # create on_call_shift with user to notify + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": timezone.datetime.now().replace(microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": timezone.timedelta(seconds=7200), } on_call_shift = make_on_call_shift( diff --git a/engine/apps/alerts/tests/test_terraform_renderer.py b/engine/apps/alerts/tests/test_terraform_renderer.py index 78ece24b..1661d89c 100644 --- a/engine/apps/alerts/tests/test_terraform_renderer.py +++ b/engine/apps/alerts/tests/test_terraform_renderer.py @@ -99,6 +99,7 @@ def test_render_terraform_file( interval=1, week_start=CustomOnCallShift.MONDAY, start=dateparse.parse_datetime("2021-08-16T17:00:00"), + rotation_start=dateparse.parse_datetime("2021-08-16T17:00:00"), duration=timezone.timedelta(seconds=3600), by_day=["MO", "SA"], rolling_users=[{user.pk: user.public_primary_key}], diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index 4d3739e1..31785994 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -132,7 +132,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): 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): + def _validate_rotation_start(self, shift_start, rotation_start): if rotation_start < shift_start: raise serializers.ValidationError({"rotation_start": ["Incorrect rotation start date"]}) @@ -162,7 +162,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): validated_data.get("interval"), validated_data.get("by_day"), ) - self._validate_rotations_start(validated_data["start"], validated_data["rotation_start"]) + self._validate_rotation_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 diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index c788d521..fe854b45 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -397,8 +397,10 @@ def test_events_calendar( name="test_calendar_schedule", ) + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": timezone.now().replace(microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": timezone.timedelta(seconds=7200), "priority_level": 2, } @@ -459,6 +461,7 @@ def test_filter_events_calendar( start_date = now - timezone.timedelta(days=7) data = { "start": start_date, + "rotation_start": start_date, "duration": timezone.timedelta(seconds=7200), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, @@ -536,6 +539,7 @@ def test_filter_events_range_calendar( start_date = now - timezone.timedelta(days=7) data = { "start": start_date, + "rotation_start": start_date, "duration": timezone.timedelta(seconds=7200), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, diff --git a/engine/apps/public_api/serializers/on_call_shifts.py b/engine/apps/public_api/serializers/on_call_shifts.py index 4471c6c0..b9ea30be 100644 --- a/engine/apps/public_api/serializers/on_call_shifts.py +++ b/engine/apps/public_api/serializers/on_call_shifts.py @@ -96,6 +96,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer "level", "start", "duration", + "rotation_start", "frequency", "interval", "until", @@ -222,6 +223,9 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer def _validate_until(self, until): self._validate_date_format(until) + def _validate_rotation_start(self, rotation_start): + self._validate_date_format(rotation_start) + def to_internal_value(self, data): if data.get("users", []) is None: # terraform case data["users"] = [] @@ -231,6 +235,8 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer data["source"] = CustomOnCallShift.SOURCE_API if data.get("start") is not None: self._validate_start(data["start"]) + if data.get("rotation_start") is not None: + self._validate_rotation_start(data["rotation_start"]) if data.get("until") is not None: self._validate_until(data["until"]) result = super().to_internal_value(data) @@ -240,6 +246,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer result = super().to_representation(instance) result["duration"] = int(instance.duration.total_seconds()) result["start"] = instance.start.strftime("%Y-%m-%dT%H:%M:%S") + result["rotation_start"] = instance.rotation_start.strftime("%Y-%m-%dT%H:%M:%S") if instance.until is not None: result["until"] = instance.until.strftime("%Y-%m-%dT%H:%M:%S") result = self._get_fields_to_represent(instance, result) @@ -331,6 +338,7 @@ class CustomOnCallShiftUpdateSerializer(CustomOnCallShiftSerializer): duration = serializers.DurationField(required=False) name = serializers.CharField(required=False) start = serializers.DateTimeField(required=False) + rotation_start = serializers.DateTimeField(required=False) team_id = TeamPrimaryKeyRelatedField(read_only=True, source="team") def update(self, instance, validated_data): diff --git a/engine/apps/public_api/tests/test_on_call_shifts.py b/engine/apps/public_api/tests/test_on_call_shifts.py index 8311c00e..0acc3fcf 100644 --- a/engine/apps/public_api/tests/test_on_call_shifts.py +++ b/engine/apps/public_api/tests/test_on_call_shifts.py @@ -2,6 +2,7 @@ import datetime import pytest from django.urls import reverse +from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient @@ -45,9 +46,11 @@ def test_get_on_call_shift(make_organization_and_user_with_token, make_on_call_s organization, user, token = make_organization_and_user_with_token() client = APIClient() + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": datetime.datetime.now().replace(microsecond=0), - "duration": datetime.timedelta(seconds=7200), + "start": start_date, + "rotation_start": start_date, + "duration": timezone.timedelta(seconds=7200), } schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) on_call_shift = make_on_call_shift( @@ -68,6 +71,7 @@ def test_get_on_call_shift(make_organization_and_user_with_token, make_on_call_s "time_zone": None, "level": 0, "start": on_call_shift.start.strftime("%Y-%m-%dT%H:%M:%S"), + "rotation_start": on_call_shift.start.strftime("%Y-%m-%dT%H:%M:%S"), "duration": int(on_call_shift.duration.total_seconds()), "users": [user.public_primary_key], } @@ -82,9 +86,11 @@ def test_get_override_on_call_shift(make_organization_and_user_with_token, make_ client = APIClient() schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": datetime.datetime.now().replace(microsecond=0), - "duration": datetime.timedelta(seconds=7200), + "start": start_date, + "rotation_start": start_date, "schedule": schedule, } on_call_shift = make_on_call_shift(organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **data) @@ -101,6 +107,7 @@ def test_get_override_on_call_shift(make_organization_and_user_with_token, make_ "type": "override", "time_zone": None, "start": on_call_shift.start.strftime("%Y-%m-%dT%H:%M:%S"), + "rotation_start": on_call_shift.start.strftime("%Y-%m-%dT%H:%M:%S"), "duration": int(on_call_shift.duration.total_seconds()), "users": [user.public_primary_key], } @@ -125,6 +132,7 @@ def test_create_on_call_shift(make_organization_and_user_with_token): "type": "recurrent_event", "level": 1, "start": start.strftime("%Y-%m-%dT%H:%M:%S"), + "rotation_start": start.strftime("%Y-%m-%dT%H:%M:%S"), "duration": 10800, "users": [user.public_primary_key], "week_start": "MO", @@ -145,6 +153,7 @@ def test_create_on_call_shift(make_organization_and_user_with_token): "time_zone": None, "level": data["level"], "start": data["start"], + "rotation_start": data["rotation_start"], "duration": data["duration"], "frequency": data["frequency"], "interval": data["interval"], @@ -174,6 +183,7 @@ def test_create_override_on_call_shift(make_organization_and_user_with_token): "name": "test name", "type": "override", "start": start.strftime("%Y-%m-%dT%H:%M:%S"), + "rotation_start": start.strftime("%Y-%m-%dT%H:%M:%S"), "duration": 10800, "users": [user.public_primary_key], } @@ -188,6 +198,7 @@ def test_create_override_on_call_shift(make_organization_and_user_with_token): "type": "override", "time_zone": None, "start": data["start"], + "rotation_start": data["rotation_start"], "duration": data["duration"], "users": [user.public_primary_key], } @@ -201,8 +212,10 @@ def test_update_on_call_shift(make_organization_and_user_with_token, make_on_cal organization, user, token = make_organization_and_user_with_token() client = APIClient() + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": datetime.datetime.now().replace(microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": datetime.timedelta(seconds=7200), "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, "interval": 2, @@ -237,6 +250,7 @@ def test_update_on_call_shift(make_organization_and_user_with_token, make_on_cal "time_zone": None, "level": 0, "start": on_call_shift.start.strftime("%Y-%m-%dT%H:%M:%S"), + "rotation_start": on_call_shift.rotation_start.strftime("%Y-%m-%dT%H:%M:%S"), "duration": data_to_update["duration"], "frequency": "weekly", "interval": on_call_shift.interval, @@ -275,8 +289,10 @@ def test_update_on_call_shift_invalid_field(make_organization_and_user_with_toke organization, user, token = make_organization_and_user_with_token() client = APIClient() + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": datetime.datetime.now().replace(microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": datetime.timedelta(seconds=7200), "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, "interval": 2, @@ -300,8 +316,10 @@ def test_delete_on_call_shift(make_organization_and_user_with_token, make_on_cal organization, user, token = make_organization_and_user_with_token() client = APIClient() + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": datetime.datetime.now().replace(microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": datetime.timedelta(seconds=7200), } on_call_shift = make_on_call_shift( diff --git a/engine/apps/public_api/tests/test_schedules.py b/engine/apps/public_api/tests/test_schedules.py index 52acd29f..b772a8ba 100644 --- a/engine/apps/public_api/tests/test_schedules.py +++ b/engine/apps/public_api/tests/test_schedules.py @@ -294,8 +294,10 @@ def test_update_calendar_schedule_with_custom_event( schedule_class=OnCallScheduleCalendar, channel=slack_channel_id, ) + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": timezone.now().replace(tzinfo=None, microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": timezone.timedelta(seconds=10800), } on_call_shift = make_on_call_shift( @@ -347,8 +349,10 @@ def test_update_calendar_schedule_invalid_override( organization, schedule_class=OnCallScheduleCalendar, ) + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": timezone.now().replace(tzinfo=None, microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": timezone.timedelta(seconds=10800), } on_call_shift = make_on_call_shift(organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **data) @@ -378,8 +382,10 @@ def test_update_web_schedule_with_override( organization, schedule_class=OnCallScheduleWeb, ) + start_date = timezone.datetime.now().replace(microsecond=0) data = { - "start": timezone.now().replace(tzinfo=None, microsecond=0), + "start": start_date, + "rotation_start": start_date, "duration": timezone.timedelta(seconds=10800), } on_call_shift = make_on_call_shift(organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **data) diff --git a/engine/apps/schedules/tests/test_ical_utils.py b/engine/apps/schedules/tests/test_ical_utils.py index 2a737df2..e7946617 100644 --- a/engine/apps/schedules/tests/test_ical_utils.py +++ b/engine/apps/schedules/tests/test_ical_utils.py @@ -41,6 +41,7 @@ def test_list_users_to_notify_from_ical_viewers_inclusion( data = { "priority_level": 1, "start": date, + "rotation_start": date, "duration": timezone.timedelta(seconds=10800), } on_call_shift = make_on_call_shift( From 24cfd3686a1bce45a616a14c5b8bfab8a4bde5fb Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 18 Jul 2022 13:29:01 +0300 Subject: [PATCH 07/12] fix tests --- engine/apps/api/tests/test_schedules.py | 2 +- engine/apps/public_api/tests/test_on_call_shifts.py | 1 + engine/apps/schedules/tests/test_custom_on_call_shift.py | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index fe854b45..aff1ec06 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -397,7 +397,7 @@ def test_events_calendar( name="test_calendar_schedule", ) - start_date = timezone.datetime.now().replace(microsecond=0) + start_date = timezone.now().replace(microsecond=0) data = { "start": start_date, "rotation_start": start_date, diff --git a/engine/apps/public_api/tests/test_on_call_shifts.py b/engine/apps/public_api/tests/test_on_call_shifts.py index 0acc3fcf..2934c6c7 100644 --- a/engine/apps/public_api/tests/test_on_call_shifts.py +++ b/engine/apps/public_api/tests/test_on_call_shifts.py @@ -91,6 +91,7 @@ def test_get_override_on_call_shift(make_organization_and_user_with_token, make_ data = { "start": start_date, "rotation_start": start_date, + "duration": datetime.timedelta(seconds=7200), "schedule": schedule, } on_call_shift = make_on_call_shift(organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **data) diff --git a/engine/apps/schedules/tests/test_custom_on_call_shift.py b/engine/apps/schedules/tests/test_custom_on_call_shift.py index 8e507c89..1782e483 100644 --- a/engine/apps/schedules/tests/test_custom_on_call_shift.py +++ b/engine/apps/schedules/tests/test_custom_on_call_shift.py @@ -15,6 +15,7 @@ def test_get_on_call_users_from_single_event(make_organization_and_user, make_on data = { "priority_level": 1, "start": date, + "rotation_start": date, "duration": timezone.timedelta(seconds=10800), } @@ -41,6 +42,7 @@ def test_get_on_call_users_from_web_schedule_override(make_organization_and_user data = { "start": date, + "rotation_start": date, "duration": timezone.timedelta(seconds=10800), "schedule": schedule, } @@ -65,6 +67,7 @@ def test_get_on_call_users_from_recurrent_event(make_organization_and_user, make data = { "priority_level": 1, "start": date, + "rotation_start": date, "duration": timezone.timedelta(seconds=10800), "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 2, @@ -107,6 +110,7 @@ def test_get_on_call_users_from_web_schedule_recurrent_event( data = { "priority_level": 1, "start": date, + "rotation_start": date, "duration": timezone.timedelta(seconds=10800), "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 2, @@ -149,6 +153,7 @@ def test_get_on_call_users_from_rolling_users_event( data = { "priority_level": 1, "start": now, + "rotation_start": now, "duration": timezone.timedelta(seconds=10800), "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 2, @@ -221,6 +226,7 @@ def test_get_oncall_users_for_multiple_schedules( shift_type=CustomOnCallShift.TYPE_SINGLE_EVENT, priority_level=1, start=now, + rotation_start=now, duration=timezone.timedelta(minutes=30), ) @@ -229,6 +235,7 @@ def test_get_oncall_users_for_multiple_schedules( shift_type=CustomOnCallShift.TYPE_SINGLE_EVENT, priority_level=1, start=now, + rotation_start=now, duration=timezone.timedelta(minutes=10), ) @@ -237,6 +244,7 @@ def test_get_oncall_users_for_multiple_schedules( shift_type=CustomOnCallShift.TYPE_SINGLE_EVENT, priority_level=1, start=now + timezone.timedelta(minutes=10), + rotation_start=now + timezone.timedelta(minutes=10), duration=timezone.timedelta(minutes=30), ) @@ -275,6 +283,7 @@ def test_shift_convert_to_ical(make_organization_and_user, make_on_call_shift): data = { "priority_level": 1, "start": date, + "rotation_start": date, "duration": timezone.timedelta(seconds=10800), "frequency": CustomOnCallShift.FREQUENCY_HOURLY, "interval": 1, From f7b356360020804893a84568ed81732b27b7e2e2 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 18 Jul 2022 15:23:15 +0300 Subject: [PATCH 08/12] Update serializers for oncall shifts --- engine/apps/api/serializers/on_call_shifts.py | 3 ++- engine/apps/public_api/serializers/on_call_shifts.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index 31785994..715c2e3a 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -163,7 +163,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): validated_data.get("by_day"), ) self._validate_rotation_start(validated_data["start"], validated_data["rotation_start"]) - self._validate_until(validated_data["rotation_start"], validated_data["until"]) + self._validate_until(validated_data["rotation_start"], validated_data.get("until")) # convert shift_end into internal value and validate raw_shift_end = self.initial_data["shift_end"] @@ -171,6 +171,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): self._validate_shift_end(validated_data["start"], shift_end) validated_data["duration"] = shift_end - validated_data["start"] + validated_data["team"] = validated_data["schedule"].team return validated_data diff --git a/engine/apps/public_api/serializers/on_call_shifts.py b/engine/apps/public_api/serializers/on_call_shifts.py index b9ea30be..d6592803 100644 --- a/engine/apps/public_api/serializers/on_call_shifts.py +++ b/engine/apps/public_api/serializers/on_call_shifts.py @@ -83,6 +83,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer required=False, child=UsersFilteredByOrganizationField(queryset=User.objects, required=False, allow_null=True), ) + rotation_start = serializers.DateTimeField(required=False) class Meta: model = CustomOnCallShift @@ -131,6 +132,8 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer validated_data.get("by_day"), validated_data.get("by_monthday"), ) + if not validated_data.get("rotation_start"): + validated_data["rotation_start"] = validated_data["start"] instance = super().create(validated_data) for schedule in instance.schedules.all(): instance.start_drop_ical_and_check_schedule_tasks(schedule) From 59e88d0c4341b8a36cb454a92b967a8f41fb4c4c Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 18 Jul 2022 15:43:57 +0300 Subject: [PATCH 09/12] fix update shift at internal api endpoint --- engine/apps/api/serializers/on_call_shifts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index 715c2e3a..15fe1a78 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -171,7 +171,8 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): self._validate_shift_end(validated_data["start"], shift_end) validated_data["duration"] = shift_end - validated_data["start"] - validated_data["team"] = validated_data["schedule"].team + if validated_data.get("schedule"): + validated_data["team"] = validated_data["schedule"].team return validated_data From b7a0411142a1d6ebd2f51987fef0f911f7c461ea Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 20 Jul 2022 13:11:16 +0300 Subject: [PATCH 10/12] Fix oncall shifts serializer, fix migration, update oncall shift log text --- engine/apps/api/serializers/on_call_shifts.py | 6 ++++-- .../migrations/0006_customoncallshift_rotation_start.py | 9 ++------- engine/apps/schedules/models/custom_on_call_shift.py | 3 ++- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index 15fe1a78..794c466f 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -46,7 +46,6 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): "until", "frequency", "interval", - "until", "by_day", "source", "rolling_users", @@ -74,7 +73,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): result = super().to_representation(instance) return result - def validate_name(self, name): # todo + def validate_name(self, name): organization = self.context["request"].auth.organization if name is None: return name @@ -147,12 +146,15 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): "interval", "by_day", "until", + "rotation_start", ] if event_type == CustomOnCallShift.TYPE_OVERRIDE: for field in fields_to_update_for_overrides: value = None if field == "priority_level": value = 0 + elif field == "rotation_start": + value = validated_data["start"] validated_data[field] = value self._validate_frequency( diff --git a/engine/apps/schedules/migrations/0006_customoncallshift_rotation_start.py b/engine/apps/schedules/migrations/0006_customoncallshift_rotation_start.py index 9facd79b..682df514 100644 --- a/engine/apps/schedules/migrations/0006_customoncallshift_rotation_start.py +++ b/engine/apps/schedules/migrations/0006_customoncallshift_rotation_start.py @@ -1,17 +1,12 @@ # Generated by Django 3.2.13 on 2022-07-12 08:03 from django.db import migrations, models +from django.db.models import F def fill_rotation_start_field(apps, schema_editor): CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift") - shifts = CustomOnCallShift.objects.all() - shifts_to_update = [] - for shift in shifts: - shift.rotation_start = shift.start - shifts_to_update.append(shift) - - CustomOnCallShift.objects.bulk_update(shifts_to_update, ["rotation_start"], batch_size=5000) + CustomOnCallShift.objects.update(rotation_start=F("start")) class Migration(migrations.Migration): diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 15c35da8..64d72ae8 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -236,7 +236,8 @@ class CustomOnCallShift(models.Model): result += ( f", frequency: {self.get_frequency_display()}, interval: {self.interval}, " f"week start: {self.week_start}, by day: {self.by_day}, by month: {self.by_month}, " - f"by monthday: {self.by_monthday}, until: {self.until.isoformat() if self.until else None}" + f"by monthday: {self.by_monthday}, rotation start: {self.rotation_start.isoformat()}, " + f"until: {self.until.isoformat() if self.until else None}" ) return result From 5205ceeede361d95ac1679c3330eb9a6159a695b Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Fri, 22 Jul 2022 14:58:27 +0400 Subject: [PATCH 11/12] More accurate invalidating of alert group web cache (#277) * Disable invalidate ag web cache on user save * Tweak invalidate_ag_web_cache on AlertReceiveChannel save --- engine/apps/alerts/models/alert_receive_channel.py | 13 +++++++++---- engine/apps/user_management/models/user.py | 9 --------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 308686c0..2f0cc016 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -694,14 +694,19 @@ def listen_for_alertreceivechannel_model_save(sender, instance, created, *args, instance.organization, None, OrganizationLogType.TYPE_HEARTBEAT_CREATED, description ) else: - logger.info(f"Drop AG cache. Reason: save alert_receive_channel {instance.pk}") if kwargs is not None: if "update_fields" in kwargs: if kwargs["update_fields"] is not None: + fields_to_not_to_invalidate_cache = [ + "rate_limit_message_task_id", + "rate_limited_in_slack_at", + "reason_to_skip_escalation", + ] # Hack to not to invalidate web cache on AlertReceiveChannel.start_send_rate_limit_message_task - if "rate_limit_message_task_id" in kwargs["update_fields"]: - return - + for f in fields_to_not_to_invalidate_cache: + if f in kwargs["update_fields"]: + return + logger.info(f"Drop AG cache. Reason: save alert_receive_channel {instance.pk}") invalidate_web_cache_for_alert_group.apply_async(kwargs={"channel_pk": instance.pk}) if instance.integration == AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING: diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index ca769243..6e8b27dc 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -8,7 +8,6 @@ from django.db.models.signals import post_save from django.dispatch import receiver from emoji import demojize -from apps.alerts.tasks import invalidate_web_cache_for_alert_group from apps.schedules.tasks import drop_cached_ical_for_custom_events_for_organization from common.constants.role import Role from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -255,14 +254,6 @@ class User(models.Model): # TODO: check whether this signal can be moved to save method of the model @receiver(post_save, sender=User) def listen_for_user_model_save(sender, instance, created, *args, **kwargs): - # if kwargs is not None: - # if "update_fields" in kwargs: - # if kwargs["update_fields"] is not None: - # if "username" not in kwargs["update_fields"]: - # return - drop_cached_ical_for_custom_events_for_organization.apply_async( (instance.organization_id,), ) - logger.info(f"Drop AG cache. Reason: save user {instance.pk}") - invalidate_web_cache_for_alert_group.apply_async(kwargs={"org_pk": instance.organization_id}) From c9c6df88c5d0f6b46eb9f0fd0f2e8aec2b5c0346 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Fri, 22 Jul 2022 15:24:24 +0400 Subject: [PATCH 12/12] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd7332e3..3546a5a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## v1.0.10 (2022-07-22) +- Speed-up of alert group web caching +- Internal api for OnCall shifts + ## v1.0.9 (2022-07-21) - Frontend bug fixes & improvements - Support regex_replace() in templates