From b1ea2b062f6b2912c4c05c5c9138e5ecceb705e9 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 13:19:12 +0300 Subject: [PATCH 1/6] Fix shift update for web schedules --- .../schedules/models/custom_on_call_shift.py | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index c4558b12..225ec544 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -433,6 +433,32 @@ class CustomOnCallShift(models.Model): return return next_event_dt + def get_last_event_date(self, date): + """Get start date of the last event before the chosen date""" + assert date >= self.start, "Chosen date should be later or equal to initial event start date" + + event_ical = self.generate_ical(self.start) + initial_event = Event.from_ical(event_ical) + # take shift interval, not event interval. For rolling_users shift it is not the same. + interval = self.interval or 1 + initial_event["rrule"]["INTERVAL"] = interval + initial_event_start = initial_event["DTSTART"].dt + + last_event = None + # repetitions generate the next event shift according with the recurrence rules + repetitions = UnfoldableCalendar(initial_event).RepeatedEvent( + initial_event, initial_event_start.replace(microsecond=0) + ) + ical_iter = repetitions.__iter__() + for event in ical_iter: + if event.start > date: + break + last_event = event + + last_event_dt = last_event.start if last_event else initial_event_start + + return last_event_dt + @cached_property def event_ical_rules(self): # e.g. {'freq': ['WEEKLY'], 'interval': [2], 'byday': ['MO', 'WE', 'FR'], 'wkst': ['SU']} @@ -498,10 +524,9 @@ class CustomOnCallShift(models.Model): self.rolling_users = result self.save(update_fields=["rolling_users"]) - def get_rotation_user_index(self, date=None): + def get_rotation_user_index(self, date): START_ROTATION_INDEX = 0 - date = timezone.now() if not date else date result = START_ROTATION_INDEX if not self.rolling_users or self.frequency is None: @@ -544,8 +569,9 @@ class CustomOnCallShift(models.Model): return last_shift def create_or_update_last_shift(self, data): + now = timezone.now().replace(microsecond=0) # rotation start date cannot be earlier than now - data["rotation_start"] = max(data["rotation_start"], timezone.now().replace(microsecond=0)) + data["rotation_start"] = max(data["rotation_start"], now) # prepare dict with params of existing instance with last updates and remove unique and m2m fields from it shift_to_update = self.last_updated_shift or self instance_data = model_to_dict(shift_to_update) @@ -557,12 +583,10 @@ class CustomOnCallShift(models.Model): instance_data["schedule"] = self.schedule instance_data["team"] = self.team # set new event start date to keep rotation index - instance_data["start"] = timezone.datetime.combine( - instance_data["rotation_start"].date(), - instance_data["start"].time(), - ).astimezone(pytz.UTC) + if instance_data["start"] == self.start: + instance_data["start"] = self.get_last_event_date(now) # calculate rotation index to keep user rotation order - start_rotation_from_user_index = self.get_rotation_user_index() + (self.start_rotation_from_user_index or 0) + start_rotation_from_user_index = self.get_rotation_user_index(now) + (self.start_rotation_from_user_index or 0) if start_rotation_from_user_index >= len(instance_data["rolling_users"]): start_rotation_from_user_index = 0 instance_data["start_rotation_from_user_index"] = start_rotation_from_user_index From d9609dbcc251ccc25c9b2f0fd85da58d9d302be0 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 13:20:28 +0300 Subject: [PATCH 2/6] Fix priority level regex, fix getting shifts without duration --- engine/apps/schedules/constants.py | 2 +- engine/apps/schedules/ical_utils.py | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/engine/apps/schedules/constants.py b/engine/apps/schedules/constants.py index 719aa0b2..a2ec8adc 100644 --- a/engine/apps/schedules/constants.py +++ b/engine/apps/schedules/constants.py @@ -9,6 +9,6 @@ ICAL_ATTENDEE = "ATTENDEE" ICAL_UID = "UID" ICAL_RRULE = "RRULE" ICAL_UNTIL = "UNTIL" -RE_PRIORITY = re.compile(r"^\[L(\d)\]") +RE_PRIORITY = re.compile(r"^\[L(\d+)\]") RE_EVENT_UID_V1 = re.compile(r"amixr-([\w\d-]+)-U(\d+)-E(\d+)-S(\d+)") RE_EVENT_UID_V2 = re.compile(r"oncall-([\w\d-]+)-PK([\w\d]+)-U(\d+)-E(\d+)-S(\d+)") diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 12fb5bd1..620e4450 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -190,18 +190,19 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ ) else: start, end = ical_events.get_start_and_end_with_respect_to_event_type(event) - result_datetime.append( - { - "start": start.astimezone(pytz.UTC), - "end": end.astimezone(pytz.UTC), - "users": users, - "missing_users": missing_users, - "priority": priority, - "source": source, - "calendar_type": calendar_type, - "shift_pk": pk, - } - ) + if start < end: + result_datetime.append( + { + "start": start.astimezone(pytz.UTC), + "end": end.astimezone(pytz.UTC), + "users": users, + "missing_users": missing_users, + "priority": priority, + "source": source, + "calendar_type": calendar_type, + "shift_pk": pk, + } + ) return result_datetime, result_date From 9b9470b358ab3a631829f1f9ff136254472c7b80 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 13:33:33 +0300 Subject: [PATCH 3/6] Fix shift update for web schedules --- engine/apps/schedules/models/custom_on_call_shift.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 225ec544..232c824a 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -441,7 +441,9 @@ class CustomOnCallShift(models.Model): initial_event = Event.from_ical(event_ical) # take shift interval, not event interval. For rolling_users shift it is not the same. interval = self.interval or 1 - initial_event["rrule"]["INTERVAL"] = interval + if "rrule" in initial_event: + # means that shift has frequency + initial_event["rrule"]["INTERVAL"] = interval initial_event_start = initial_event["DTSTART"].dt last_event = None From 7571bfa62521ed0d419baf29a691355a59c48524 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 14:13:47 +0300 Subject: [PATCH 4/6] Fix tests for shift update --- engine/apps/api/tests/test_oncall_shift.py | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index ddd21498..efa2fb96 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -412,8 +412,9 @@ def test_update_old_on_call_shift_with_future_version( token, user1, user2, organization, schedule = on_call_shift_internal_api_setup client = APIClient() - start_date = (timezone.now() - timezone.timedelta(days=3)).replace(microsecond=0) - next_rotation_start_date = start_date + timezone.timedelta(days=5) + now = timezone.now().replace(microsecond=0) + start_date = now - timezone.timedelta(days=3) + next_rotation_start_date = now + timezone.timedelta(days=1) updated_duration = timezone.timedelta(hours=4) title = "Test Shift Rotation" @@ -422,10 +423,11 @@ def test_update_old_on_call_shift_with_future_version( shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, title=title, - start=start_date, + start=next_rotation_start_date, duration=timezone.timedelta(hours=3), rotation_start=next_rotation_start_date, rolling_users=[{user1.pk: user1.public_primary_key}], + frequency=CustomOnCallShift.FREQUENCY_DAILY, ) old_on_call_shift = make_on_call_shift( schedule.organization, @@ -438,6 +440,7 @@ def test_update_old_on_call_shift_with_future_version( until=next_rotation_start_date, rolling_users=[{user1.pk: user1.public_primary_key}], updated_shift=new_on_call_shift, + frequency=CustomOnCallShift.FREQUENCY_DAILY, ) # update shift_end and priority_level data_to_update = { @@ -445,9 +448,9 @@ def test_update_old_on_call_shift_with_future_version( "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + updated_duration).strftime("%Y-%m-%dT%H:%M:%SZ"), - "rotation_start": next_rotation_start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, - "frequency": None, + "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key]], @@ -461,27 +464,28 @@ def test_update_old_on_call_shift_with_future_version( url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": old_on_call_shift.public_primary_key}) response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) + response_data = response.json() - next_shift_start_date = timezone.datetime.combine(next_rotation_start_date.date(), start_date.time()).astimezone( - timezone.pytz.UTC - ) + for key in ["shift_start", "shift_end", "rotation_start"]: + data_to_update.pop(key) + response_data.pop(key) expected_payload = data_to_update | { "id": new_on_call_shift.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "updated_shift": None, - "shift_start": next_shift_start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), - "shift_end": (next_shift_start_date + updated_duration).strftime("%Y-%m-%dT%H:%M:%SZ"), } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload - new_on_call_shift.refresh_from_db() - # check if the newest version of shift was changed assert old_on_call_shift.duration != updated_duration assert old_on_call_shift.priority_level != data_to_update["priority_level"] + new_on_call_shift.refresh_from_db() + # check if the newest version of shift was changed + assert new_on_call_shift.start - now < timezone.timedelta(minutes=1) + assert new_on_call_shift.rotation_start - now < timezone.timedelta(minutes=1) assert new_on_call_shift.duration == updated_duration assert new_on_call_shift.priority_level == data_to_update["priority_level"] From edba707b42ad6a0159035a81ce4c4236df1f219a Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 14:19:32 +0300 Subject: [PATCH 5/6] Fix priority level test --- engine/apps/slack/tests/test_parse_slack_usernames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/slack/tests/test_parse_slack_usernames.py b/engine/apps/slack/tests/test_parse_slack_usernames.py index df43c950..659cc27a 100644 --- a/engine/apps/slack/tests/test_parse_slack_usernames.py +++ b/engine/apps/slack/tests/test_parse_slack_usernames.py @@ -52,5 +52,5 @@ def test_remove_priority_from_username(): assert parse_username_from_string("[L1] bob") == "bob" assert parse_username_from_string(" [L1] bob ") == "bob" assert parse_username_from_string("[L2] bob[L1]") == "bob[L1]" - assert parse_username_from_string("[L27]bob") == "[L27]bob" + assert parse_username_from_string("[L27]bob") == "bob" assert parse_username_from_string("[[L2]] bob[[[L1]") == "[[L2]] bob[[[L1]" From 996e6076ab84376eb7a9911c7419c53e76ffa2b7 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 16:42:58 +0300 Subject: [PATCH 6/6] Remove unnecessary variable --- engine/apps/schedules/models/custom_on_call_shift.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 232c824a..3c7c41e9 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -411,8 +411,7 @@ class CustomOnCallShift(models.Model): repetitions = UnfoldableCalendar(current_event).RepeatedEvent( current_event, next_event_start.replace(microsecond=0) ) - ical_iter = repetitions.__iter__() - for event in ical_iter: + for event in repetitions.__iter__(): if end_date: # end_date exists for long events with frequency weekly and monthly if end_date >= event.start >= next_event_start: if ( @@ -451,8 +450,7 @@ class CustomOnCallShift(models.Model): repetitions = UnfoldableCalendar(initial_event).RepeatedEvent( initial_event, initial_event_start.replace(microsecond=0) ) - ical_iter = repetitions.__iter__() - for event in ical_iter: + for event in repetitions.__iter__(): if event.start > date: break last_event = event