From 87fda3caf67b85b0ca5ffa5da5ee86216cd7ee8f Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Wed, 6 Jul 2022 15:47:21 -0300 Subject: [PATCH] Use shift public key in events --- engine/apps/api/tests/test_schedules.py | 16 +++-- engine/apps/api/views/schedule.py | 4 +- .../public_api/serializers/schedules_web.py | 3 +- engine/apps/schedules/ical_utils.py | 36 ++++++++---- .../schedules/models/custom_on_call_shift.py | 2 +- .../apps/schedules/models/on_call_schedule.py | 58 ++----------------- .../apps/schedules/tests/test_ical_utils.py | 21 ++++++- 7 files changed, 67 insertions(+), 73 deletions(-) diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index b0e61d54..c788d521 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -429,7 +429,9 @@ def test_events_calendar( "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, - "shift_uuid": str(on_call_shift.uuid), + "shift": { + "pk": on_call_shift.public_primary_key, + }, } ], } @@ -490,7 +492,9 @@ def test_filter_events_calendar( "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, - "shift_uuid": str(on_call_shift.uuid), + "shift": { + "pk": on_call_shift.public_primary_key, + }, }, { "all_day": False, @@ -502,7 +506,9 @@ def test_filter_events_calendar( "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, - "shift_uuid": str(on_call_shift.uuid), + "shift": { + "pk": on_call_shift.public_primary_key, + }, }, ], } @@ -566,7 +572,9 @@ def test_filter_events_range_calendar( "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, - "shift_uuid": str(on_call_shift.uuid), + "shift": { + "pk": on_call_shift.public_primary_key, + }, } ], } diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index 819950eb..4839ecd8 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -217,7 +217,9 @@ class ScheduleView( "calendar_type": shift["calendar_type"], "is_empty": len(shift["users"]) == 0 and not is_gap, "is_gap": is_gap, - "shift_uuid": shift["shift_uuid"], + "shift": { + "pk": shift["shift_pk"], + }, } events.append(shift_json) diff --git a/engine/apps/public_api/serializers/schedules_web.py b/engine/apps/public_api/serializers/schedules_web.py index 2ed1eed8..4d9c6b84 100644 --- a/engine/apps/public_api/serializers/schedules_web.py +++ b/engine/apps/public_api/serializers/schedules_web.py @@ -1,5 +1,4 @@ import pytz -from django.utils import timezone from rest_framework import serializers from apps.public_api.serializers.schedules_base import ScheduleBaseSerializer @@ -35,7 +34,7 @@ class ScheduleWebSerializer(ScheduleBaseSerializer): def validate_time_zone(self, tz): try: - timezone.now().astimezone(pytz.timezone(tz)) + pytz.timezone(tz) except pytz.exceptions.UnknownTimeZoneError: raise BadRequest(detail="Invalid time zone") return tz diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 8bd1c967..65b6d574 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -75,7 +75,8 @@ ICAL_DESCRIPTION = "DESCRIPTION" ICAL_ATTENDEE = "ATTENDEE" ICAL_UID = "UID" RE_PRIORITY = re.compile(r"^\[L(\d)\]") -RE_EVENT_UID = re.compile(r"amixr-([\w\d-]+)-U(\d+)-E(\d+)-S(\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+)") logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -140,7 +141,7 @@ def list_of_oncall_shifts_from_ical( "source": None, "calendar_type": None, "is_gap": True, - "shift_uuid": None, + "shift_pk": None, } ) result = sorted(result_datetime, key=lambda dt: dt["start"]) + result_date @@ -154,7 +155,7 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ result_date = [] for event in events: priority = parse_priority_from_string(event.get(ICAL_SUMMARY, "[L0]")) - uuid, source = parse_event_uid(event.get(ICAL_UID)) + pk, source = parse_event_uid(event.get(ICAL_UID)) users = get_users_from_ical_event(event, schedule.organization) # Define on-call shift out of ical event that has the actual user if len(users) > 0 or with_empty_shifts: @@ -170,7 +171,7 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ "priority": priority, "source": source, "calendar_type": calendar_type, - "shift_uuid": uuid, + "shift_pk": pk, } ) else: @@ -185,7 +186,7 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ "priority": priority, "source": source, "calendar_type": calendar_type, - "shift_uuid": uuid, + "shift_pk": pk, } ) return result_datetime, result_date @@ -193,7 +194,7 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ EmptyShift = namedtuple( "EmptyShift", - ["start", "end", "summary", "description", "attendee", "all_day", "calendar_type", "calendar_tz", "shift_uuid"], + ["start", "end", "summary", "description", "attendee", "all_day", "calendar_type", "calendar_tz", "shift_pk"], ) @@ -237,7 +238,7 @@ def list_of_empty_shifts_in_schedule(schedule, start_date, end_date): summary = event.get(ICAL_SUMMARY, "") description = event.get(ICAL_DESCRIPTION, "") attendee = event.get(ICAL_ATTENDEE, "") - uuid, _ = parse_event_uid(event.get(ICAL_UID)) + pk, _ = parse_event_uid(event.get(ICAL_UID)) event_hash = hash(f"{event[ICAL_UID]}{summary}{description}{attendee}") if event_hash in checked_events: @@ -265,7 +266,7 @@ def list_of_empty_shifts_in_schedule(schedule, start_date, end_date): all_day=all_day, calendar_type=calendar_type, calendar_tz=calendar_tz, - shift_uuid=uuid, + shift_pk=pk, ) ) empty_shifts.extend(empty_shifts_per_calendar) @@ -340,15 +341,26 @@ def parse_priority_from_string(string): def parse_event_uid(string): - uuid = None + pk = None + source = None source_verbal = None - match = RE_EVENT_UID.match(string) + + match = RE_EVENT_UID_V2.match(string) if match: - uuid, _, _, source = match.groups() + _, pk, _, _, source = match.groups() + else: + # eventually this path would be automatically deprecated + # once all ical representations are refreshed + match = RE_EVENT_UID_V1.match(string) + if match: + _, _, _, source = match.groups() + + if source is not None: source = int(source) CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift") source_verbal = CustomOnCallShift.SOURCE_CHOICES[source][1] - return uuid, source_verbal + + return pk, source_verbal def get_usernames_from_ical_event(event): diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index aa721d28..0f2753da 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -230,7 +230,7 @@ class CustomOnCallShift(models.Model): def generate_ical(self, user, start, user_counter, counter=1, time_zone="UTC"): # create event for each user in a list because we can't parse multiple users from ical summary event = Event() - event["uid"] = f"amixr-{self.uuid}-U{user_counter}-E{counter}-S{self.source}" + event["uid"] = f"oncall-{self.uuid}-PK{self.public_primary_key}-U{user_counter}-E{counter}-S{self.source}" event.add("summary", self.get_summary_with_user_for_ical(user)) event.add("dtstart", self.convert_dt_to_schedule_timezone(start, time_zone)) event.add("dtend", self.convert_dt_to_schedule_timezone(start + self.duration, time_zone)) diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index 9e84d8d3..05493789 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -213,10 +213,14 @@ class OnCallSchedule(PolymorphicModel): raise NotImplementedError def _drop_primary_ical_file(self): - raise NotImplementedError + self.prev_ical_file_primary = self.cached_ical_file_primary + self.cached_ical_file_primary = None + self.save(update_fields=["cached_ical_file_primary", "prev_ical_file_primary"]) def _drop_overrides_ical_file(self): - raise NotImplementedError + self.prev_ical_file_overrides = self.cached_ical_file_overrides + self.cached_ical_file_overrides = None + self.save(update_fields=["cached_ical_file_overrides", "prev_ical_file_overrides"]) class OnCallScheduleICal(OnCallSchedule): @@ -255,26 +259,6 @@ class OnCallScheduleICal(OnCallSchedule): cached_ical_file = self.cached_ical_file_overrides return cached_ical_file - def _drop_primary_ical_file(self): - self.prev_ical_file_primary = self.cached_ical_file_primary - self.cached_ical_file_primary = None - self.save( - update_fields=[ - "cached_ical_file_primary", - "prev_ical_file_primary", - ] - ) - - def _drop_overrides_ical_file(self): - self.prev_ical_file_overrides = self.cached_ical_file_overrides - self.cached_ical_file_overrides = None - self.save( - update_fields=[ - "cached_ical_file_overrides", - "prev_ical_file_overrides", - ] - ) - def _refresh_primary_ical_file(self): self.prev_ical_file_primary = self.cached_ical_file_primary if self.ical_url_primary is not None: @@ -351,26 +335,6 @@ class OnCallScheduleCalendar(OnCallSchedule): ) self.save(update_fields=["cached_ical_file_overrides", "prev_ical_file_overrides", "ical_file_error_overrides"]) - def _drop_primary_ical_file(self): - self.prev_ical_file_primary = self.cached_ical_file_primary - self.cached_ical_file_primary = None - self.save( - update_fields=[ - "cached_ical_file_primary", - "prev_ical_file_primary", - ] - ) - - def _drop_overrides_ical_file(self): - self.prev_ical_file_overrides = self.cached_ical_file_overrides - self.cached_ical_file_overrides = None - self.save( - update_fields=[ - "cached_ical_file_overrides", - "prev_ical_file_overrides", - ] - ) - def _generate_ical_file_primary(self): """ Generate iCal events file from custom on-call shifts (created via API) @@ -438,11 +402,6 @@ class OnCallScheduleWeb(OnCallSchedule): self.cached_ical_file_primary = self._generate_ical_file_primary() self.save(update_fields=["cached_ical_file_primary", "prev_ical_file_primary"]) - def _drop_primary_ical_file(self): - self.prev_ical_file_primary = self.cached_ical_file_primary - self.cached_ical_file_primary = None - self.save(update_fields=["cached_ical_file_primary", "prev_ical_file_primary"]) - @cached_property def _ical_file_overrides(self): """Return cached ical file with iCal events from custom on-call overrides shifts.""" @@ -455,8 +414,3 @@ class OnCallScheduleWeb(OnCallSchedule): self.prev_ical_file_overrides = self.cached_ical_file_overrides self.cached_ical_file_overrides = self._generate_ical_file_overrides() self.save(update_fields=["cached_ical_file_overrides", "prev_ical_file_overrides"]) - - def _drop_overrides_ical_file(self): - self.prev_ical_file_overrides = self.cached_ical_file_overrides - self.cached_ical_file_overrides = None - self.save(update_fields=["cached_ical_file_overrides", "prev_ical_file_overrides"]) diff --git a/engine/apps/schedules/tests/test_ical_utils.py b/engine/apps/schedules/tests/test_ical_utils.py index 8032334d..2a737df2 100644 --- a/engine/apps/schedules/tests/test_ical_utils.py +++ b/engine/apps/schedules/tests/test_ical_utils.py @@ -1,7 +1,9 @@ +from uuid import uuid4 + import pytest from django.utils import timezone -from apps.schedules.ical_utils import list_users_to_notify_from_ical, users_in_ical +from apps.schedules.ical_utils import list_users_to_notify_from_ical, parse_event_uid, users_in_ical from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar from common.constants.role import Role @@ -58,3 +60,20 @@ def test_list_users_to_notify_from_ical_viewers_inclusion( else: assert len(users_on_call) == 1 assert set(users_on_call) == {user} + + +def test_parse_event_uid_v1(): + uuid = uuid4() + event_uid = f"amixr-{uuid}-U1-E2-S1" + pk, source = parse_event_uid(event_uid) + assert pk is None + assert source == "api" + + +def test_parse_event_uid_v2(): + uuid = uuid4() + pk_value = "OABCDEF12345" + event_uid = f"oncall-{uuid}-PK{pk_value}-U3-E1-S2" + pk, source = parse_event_uid(event_uid) + assert pk == pk_value + assert source == "slack"