diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index 04de4393..7ecebaf4 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -264,7 +264,13 @@ class ScheduleView( if filter_by is not None and filter_by != EVENTS_FILTER_BY_FINAL: filter_by = OnCallSchedule.PRIMARY if filter_by == EVENTS_FILTER_BY_ROTATION else OnCallSchedule.OVERRIDES events = schedule.filter_events( - user_tz, starting_date, days=days, with_empty=True, with_gap=resolve_schedule, filter_by=filter_by + user_tz, + starting_date, + days=days, + with_empty=True, + with_gap=resolve_schedule, + filter_by=filter_by, + all_day_datetime=True, ) else: # return final schedule events = schedule.final_events(user_tz, starting_date, days) diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index 24aa8d0d..bd79c190 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -202,7 +202,16 @@ class OnCallSchedule(PolymorphicModel): """Return public primary keys for all users referenced in the schedule.""" return set() - def filter_events(self, user_timezone, starting_date, days, with_empty=False, with_gap=False, filter_by=None): + def filter_events( + self, + user_timezone, + starting_date, + days, + with_empty=False, + with_gap=False, + filter_by=None, + all_day_datetime=False, + ): """Return filtered events from schedule.""" shifts = ( list_of_oncall_shifts_from_ical( @@ -212,13 +221,18 @@ class OnCallSchedule(PolymorphicModel): ) events = [] for shift in shifts: - all_day = type(shift["start"]) == datetime.date + start = shift["start"] + all_day = type(start) == datetime.date + # fix confusing end date for all-day event + end = shift["end"] - timezone.timedelta(days=1) if all_day else shift["end"] + if all_day and all_day_datetime: + start = datetime.datetime.combine(start, datetime.datetime.min.time(), tzinfo=pytz.UTC) + end = datetime.datetime.combine(end, datetime.datetime.max.time(), tzinfo=pytz.UTC) is_gap = shift.get("is_gap", False) shift_json = { "all_day": all_day, - "start": shift["start"], - # fix confusing end date for all-day event - "end": shift["end"] - timezone.timedelta(days=1) if all_day else shift["end"], + "start": start, + "end": end, "users": [ { "display_name": user.username, @@ -246,7 +260,9 @@ class OnCallSchedule(PolymorphicModel): def final_events(self, user_tz, starting_date, days): """Return schedule final events, after resolving shifts and overrides.""" - events = self.filter_events(user_tz, starting_date, days=days, with_empty=True, with_gap=True) + events = self.filter_events( + user_tz, starting_date, days=days, with_empty=True, with_gap=True, all_day_datetime=True + ) events = self._resolve_schedule(events) return events diff --git a/engine/apps/schedules/tests/calendars/calendar_with_all_day_event.ics b/engine/apps/schedules/tests/calendars/calendar_with_all_day_event.ics index 4d0e562c..cbb31f9d 100644 --- a/engine/apps/schedules/tests/calendars/calendar_with_all_day_event.ics +++ b/engine/apps/schedules/tests/calendars/calendar_with_all_day_event.ics @@ -30,6 +30,20 @@ SUMMARY:@Alex TRANSP:TRANSPARENT END:VEVENT BEGIN:VEVENT +DTSTART;VALUE=DATE:20210127 +DTEND;VALUE=DATE:20210129 +DTSTAMP:20210127T154139Z +UID:7q00jpu4hdlr9e3j4fftbv7kt8@google.com +CREATED:20210127T143802Z +DESCRIPTION: +LAST-MODIFIED:20210127T143802Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:@Alice +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT DTSTART;TZID=Asia/Yekaterinburg:20210127T130000 DTEND;TZID=Asia/Yekaterinburg:20210127T220000 DTSTAMP:20210127T154139Z diff --git a/engine/apps/schedules/tests/test_ical_proxy.py b/engine/apps/schedules/tests/test_ical_proxy.py index 4dc06208..1673a657 100644 --- a/engine/apps/schedules/tests/test_ical_proxy.py +++ b/engine/apps/schedules/tests/test_ical_proxy.py @@ -45,8 +45,9 @@ def test_recurring_ical_events_with_all_day_event(get_ical): parsed_iso_day_to_check - timezone.timedelta(days=1), parsed_iso_day_to_check + timezone.timedelta(days=1), ) - assert len(events) == 4 + assert len(events) == 5 assert events[0]["SUMMARY"] == "@Alex" - assert events[1]["SUMMARY"] == "@Bob" - assert events[2]["SUMMARY"] == "@Bernard Desruisseaux" + assert events[1]["SUMMARY"] == "@Alice" + assert events[2]["SUMMARY"] == "@Bob" assert events[3]["SUMMARY"] == "@Bernard Desruisseaux" + assert events[4]["SUMMARY"] == "@Bernard Desruisseaux" diff --git a/engine/apps/schedules/tests/test_ical_utils.py b/engine/apps/schedules/tests/test_ical_utils.py index b1d7171f..bb13cf5b 100644 --- a/engine/apps/schedules/tests/test_ical_utils.py +++ b/engine/apps/schedules/tests/test_ical_utils.py @@ -91,7 +91,7 @@ def test_shifts_dict_all_day_middle_event(make_organization, make_schedule, get_ parsed_iso_day_to_check = datetime.datetime.fromisoformat(day_to_check_iso).replace(tzinfo=pytz.UTC) requested_date = (parsed_iso_day_to_check - timezone.timedelta(days=1)).date() shifts = list_of_oncall_shifts_from_ical(schedule, requested_date, days=3, with_empty_shifts=True) - assert len(shifts) == 4 + assert len(shifts) == 5 for s in shifts: start = s["start"].date() if isinstance(s["start"], datetime.datetime) else s["start"] end = s["end"].date() if isinstance(s["end"], datetime.datetime) else s["end"] diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index 8bb079f4..a4a913e9 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -235,7 +235,7 @@ def test_filter_events_ical_all_day(make_organization, make_user_for_organizatio organization = make_organization() schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) schedule.cached_ical_file_primary = calendar.to_ical() - for u in ("@Bernard Desruisseaux", "@Bob", "@Alex"): + for u in ("@Bernard Desruisseaux", "@Bob", "@Alex", "@Alice"): make_user_for_organization(organization, username=u) # clear users pks <-> organization cache (persisting between tests) memoized_users_in_ical.cache_clear() @@ -246,14 +246,44 @@ def test_filter_events_ical_all_day(make_organization, make_user_for_organizatio events = schedule.final_events("UTC", start_date, days=2) expected_events = [ - # all_day, users, start - (False, ["@Bernard Desruisseaux"], datetime.datetime(2021, 1, 26, 8, 0, tzinfo=pytz.UTC)), - (True, ["@Alex"], datetime.date(2021, 1, 27)), - (False, ["@Bob"], datetime.datetime(2021, 1, 27, 8, 0, tzinfo=pytz.UTC)), + # all_day, users, start, end + ( + False, + ["@Bernard Desruisseaux"], + datetime.datetime(2021, 1, 26, 8, 0, tzinfo=pytz.UTC), + datetime.datetime(2021, 1, 26, 17, 0, tzinfo=pytz.UTC), + ), + ( + True, + ["@Alex"], + datetime.datetime(2021, 1, 27, 0, 0, tzinfo=pytz.UTC), + datetime.datetime(2021, 1, 27, 23, 59, 59, 999999, tzinfo=pytz.UTC), + ), + ( + True, + ["@Alice"], + datetime.datetime(2021, 1, 27, 0, 0, tzinfo=pytz.UTC), + datetime.datetime(2021, 1, 28, 23, 59, 59, 999999, tzinfo=pytz.UTC), + ), + ( + False, + ["@Bob"], + datetime.datetime(2021, 1, 27, 8, 0, tzinfo=pytz.UTC), + datetime.datetime(2021, 1, 27, 17, 0, tzinfo=pytz.UTC), + ), + ] + expected = [ + {"all_day": all_day, "users": users, "start": start, "end": end} + for all_day, users, start, end in expected_events ] - expected = [{"all_day": all_day, "users": users, "start": start} for all_day, users, start in expected_events] returned = [ - {"all_day": e["all_day"], "users": [u["display_name"] for u in e["users"]], "start": e["start"]} for e in events + { + "all_day": e["all_day"], + "users": [u["display_name"] for u in e["users"]], + "start": e["start"], + "end": e["end"], + } + for e in events ] assert returned == expected