Merge branch 'dev' into on-call-shifts-rotation-index
# Conflicts: # engine/apps/schedules/models/on_call_schedule.py
This commit is contained in:
commit
67eb0b423d
6 changed files with 83 additions and 72 deletions
|
|
@ -9,9 +9,9 @@ from django.utils import timezone
|
|||
from apps.schedules.ical_events import ical_events
|
||||
from apps.schedules.ical_utils import (
|
||||
calculate_shift_diff,
|
||||
event_start_end_all_day_with_respect_to_type,
|
||||
get_icalendar_tz_or_utc,
|
||||
get_usernames_from_ical_event,
|
||||
ical_date_to_datetime,
|
||||
is_icals_equal,
|
||||
memoized_users_in_ical,
|
||||
)
|
||||
|
|
@ -35,12 +35,7 @@ def get_current_shifts_from_ical(calendar, schedule, min_priority=0):
|
|||
usernames, priority = get_usernames_from_ical_event(event)
|
||||
users = memoized_users_in_ical(tuple(usernames), schedule.organization)
|
||||
if len(users) > 0:
|
||||
event_start, start_all_day = ical_date_to_datetime(
|
||||
event["DTSTART"].dt,
|
||||
calendar_tz,
|
||||
start=True,
|
||||
)
|
||||
event_end, end_all_day = ical_date_to_datetime(event["DTEND"].dt, calendar_tz, start=False)
|
||||
event_start, event_end, all_day_event = event_start_end_all_day_with_respect_to_type(event, calendar_tz)
|
||||
|
||||
if event["UID"] in shifts:
|
||||
existing_event = shifts[event["UID"]]
|
||||
|
|
@ -50,7 +45,7 @@ def get_current_shifts_from_ical(calendar, schedule, min_priority=0):
|
|||
"users": [u.pk for u in users],
|
||||
"start": event_start,
|
||||
"end": event_end,
|
||||
"all_day": start_all_day,
|
||||
"all_day": all_day_event,
|
||||
"priority": priority + min_priority, # increase priority for overrides
|
||||
"priority_increased_by": min_priority,
|
||||
}
|
||||
|
|
@ -70,19 +65,14 @@ def get_next_shifts_from_ical(calendar, schedule, min_priority=0, days_to_lookup
|
|||
usernames, priority = get_usernames_from_ical_event(event)
|
||||
users = memoized_users_in_ical(tuple(usernames), schedule.organization)
|
||||
if len(users) > 0:
|
||||
event_start, start_all_day = ical_date_to_datetime(
|
||||
event["DTSTART"].dt,
|
||||
calendar_tz,
|
||||
start=True,
|
||||
)
|
||||
event_end, end_all_day = ical_date_to_datetime(event["DTEND"].dt, calendar_tz, start=False)
|
||||
event_start, event_end, all_day_event = event_start_end_all_day_with_respect_to_type(event, calendar_tz)
|
||||
|
||||
# next_shifts are not stored in db so we can use User objects directly
|
||||
shifts[f"{event_start.timestamp()}_{event['UID']}"] = {
|
||||
"users": users,
|
||||
"start": event_start,
|
||||
"end": event_end,
|
||||
"all_day": start_all_day,
|
||||
"all_day": all_day_event,
|
||||
"priority": priority + min_priority, # increase priority for overrides
|
||||
"priority_increased_by": min_priority,
|
||||
}
|
||||
|
|
@ -265,7 +255,7 @@ def notify_ical_schedule_shift(schedule_pk):
|
|||
|
||||
for prev_ical_file, current_ical_file in prev_and_current_ical_files:
|
||||
if prev_ical_file is not None and (
|
||||
current_ical_file is None or not is_icals_equal(current_ical_file, prev_ical_file, schedule)
|
||||
current_ical_file is None or not is_icals_equal(current_ical_file, prev_ical_file)
|
||||
):
|
||||
# If icals are not equal then compare current_events from them
|
||||
is_prev_ical_diff = True
|
||||
|
|
|
|||
|
|
@ -1517,7 +1517,7 @@ def test_on_call_shift_preview_update(
|
|||
|
||||
now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
start_date = now - timezone.timedelta(days=7)
|
||||
request_date = start_date
|
||||
tomorrow = now + timezone.timedelta(days=1)
|
||||
|
||||
user = make_user_for_organization(organization)
|
||||
other_user = make_user_for_organization(organization)
|
||||
|
|
@ -1536,11 +1536,9 @@ def test_on_call_shift_preview_update(
|
|||
)
|
||||
on_call_shift.add_rolling_users([[user]])
|
||||
|
||||
url = "{}?date={}&days={}".format(
|
||||
reverse("api-internal:oncall_shifts-preview"), request_date.strftime("%Y-%m-%d"), 1
|
||||
)
|
||||
shift_start = (start_date + timezone.timedelta(hours=10)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
shift_end = (start_date + timezone.timedelta(hours=18)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
url = "{}?date={}&days={}".format(reverse("api-internal:oncall_shifts-preview"), tomorrow.strftime("%Y-%m-%d"), 1)
|
||||
shift_start = (tomorrow + timezone.timedelta(hours=10)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
shift_end = (tomorrow + timezone.timedelta(hours=18)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
shift_data = {
|
||||
"schedule": schedule.public_primary_key,
|
||||
"shift_pk": on_call_shift.public_primary_key,
|
||||
|
|
@ -1557,39 +1555,43 @@ def test_on_call_shift_preview_update(
|
|||
|
||||
# check rotation events
|
||||
rotation_events = response.json()["rotation"]
|
||||
assert len(rotation_events) == 4
|
||||
# the final original rotation events are returned and the ID is kept
|
||||
for shift in rotation_events[:3]:
|
||||
assert shift["shift"]["pk"] == on_call_shift.public_primary_key
|
||||
# previewing an update does not reuse shift PK if rotation already started
|
||||
shift_pk = rotation_events[0]["shift"]["pk"]
|
||||
assert shift_pk != on_call_shift.public_primary_key
|
||||
expected_rotation_events = [
|
||||
{
|
||||
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
|
||||
"shift": {"pk": shift_pk},
|
||||
"start": shift_start,
|
||||
"end": shift_end,
|
||||
"all_day": False,
|
||||
"is_override": False,
|
||||
"is_empty": False,
|
||||
"is_gap": False,
|
||||
"priority_level": 1,
|
||||
"missing_users": [],
|
||||
"users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}],
|
||||
"source": "web",
|
||||
},
|
||||
]
|
||||
assert rotation_events == expected_rotation_events
|
||||
new_shift_pk = rotation_events[-1]["shift"]["pk"]
|
||||
assert new_shift_pk != on_call_shift.public_primary_key
|
||||
expected_shift_preview = {
|
||||
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
|
||||
"shift": {"pk": new_shift_pk},
|
||||
"start": shift_start,
|
||||
"end": shift_end,
|
||||
"all_day": False,
|
||||
"is_override": False,
|
||||
"is_empty": False,
|
||||
"is_gap": False,
|
||||
"priority_level": 1,
|
||||
"missing_users": [],
|
||||
"users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}],
|
||||
"source": "web",
|
||||
}
|
||||
assert rotation_events[-1] == expected_shift_preview
|
||||
|
||||
# check final schedule events
|
||||
final_events = response.json()["final"]
|
||||
expected = (
|
||||
# start (h), duration (H), user, priority
|
||||
(0, 1, user.username, 1), # 0-1 user
|
||||
(4, 1, user.username, 1), # 4-5 user
|
||||
(8, 1, user.username, 1), # 8-9 user
|
||||
(10, 8, other_user.username, 1), # 10-18 other_user
|
||||
)
|
||||
expected_events = [
|
||||
{
|
||||
"end": (start_date + timezone.timedelta(hours=start + duration)).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"end": (tomorrow + timezone.timedelta(hours=start + duration)).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"priority_level": priority,
|
||||
"start": (start_date + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0)).strftime(
|
||||
"start": (tomorrow + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0)).strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
),
|
||||
"user": user,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from django.utils.functional import cached_property
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
|
@ -63,6 +64,8 @@ class ScheduleView(
|
|||
"related_escalation_chains",
|
||||
),
|
||||
}
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ("name",)
|
||||
|
||||
queryset = OnCallSchedule.objects.all()
|
||||
serializer_class = PolymorphicScheduleSerializer
|
||||
|
|
|
|||
|
|
@ -259,15 +259,10 @@ def list_of_empty_shifts_in_schedule(schedule, start_date, end_date):
|
|||
|
||||
checked_events.add(event_hash)
|
||||
|
||||
all_day = False
|
||||
if type(event[ICAL_DATETIME_START].dt) == datetime.date:
|
||||
# Convert all-day events start and end from date to datetime with calendar's tz
|
||||
start, _ = ical_date_to_datetime(event["DTSTART"].dt, calendar_tz, start=True)
|
||||
end, _ = ical_date_to_datetime(event["DTEND"].dt, calendar_tz, start=False)
|
||||
all_day = True
|
||||
else:
|
||||
start = event[ICAL_DATETIME_START].dt.astimezone(pytz.UTC)
|
||||
end = event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC)
|
||||
start, end, all_day = event_start_end_all_day_with_respect_to_type(event, calendar_tz)
|
||||
if not all_day:
|
||||
start = start.astimezone(pytz.UTC)
|
||||
end = end.astimezone(pytz.UTC)
|
||||
|
||||
empty_shifts_per_calendar.append(
|
||||
EmptyShift(
|
||||
|
|
@ -428,28 +423,32 @@ def is_icals_equal_line_by_line(first, second):
|
|||
return True
|
||||
|
||||
|
||||
def is_icals_equal(first, second, schedule):
|
||||
from apps.schedules.models import OnCallScheduleICal # noqa
|
||||
|
||||
if isinstance(schedule, OnCallScheduleICal):
|
||||
first_cal = Calendar.from_ical(first)
|
||||
def is_icals_equal(first, second):
|
||||
first_cal = Calendar.from_ical(first)
|
||||
if first_cal.get("PRODID", None) in ("-//My calendar product//amixr//", "-//web schedule//oncall//"):
|
||||
# Compare schedules generated by oncall line by line, since they not support SEQUENCE field yet.
|
||||
# But we are sure that same calendars will have same lines, since we are generating it.
|
||||
return is_icals_equal_line_by_line(first, second)
|
||||
else:
|
||||
# Compare external calendars by events, since sometimes they contain different lines even for equal calendars.
|
||||
second_cal = Calendar.from_ical(second)
|
||||
first_subcomponents = first_cal.subcomponents
|
||||
second_subcomponents = second_cal.subcomponents
|
||||
if len(first_subcomponents) != len(second_subcomponents):
|
||||
return False
|
||||
for idx, first_cmp in enumerate(first_cal.subcomponents):
|
||||
second_cmp = second_subcomponents[idx]
|
||||
if first_cmp.name == second_cmp.name == "VEVENT":
|
||||
first_uid, first_seq = first_cmp.get("UID", None), first_cmp.get("SEQUENCE", None)
|
||||
second_uid, second_seq = second_cmp.get("UID", None), second_cmp.get("SEQUENCE", None)
|
||||
if first_uid != second_uid:
|
||||
return False
|
||||
elif first_seq != second_seq:
|
||||
return False
|
||||
first_cal_events = {}
|
||||
second_cal_events = {}
|
||||
for cmp in first_cal.subcomponents:
|
||||
first_cal_events[cmp.get("UID", None)] = cmp.get("SEQUENCE", None)
|
||||
for cmp in second_cal.subcomponents:
|
||||
second_cal_events[cmp.get("UID", None)] = cmp.get("SEQUENCE", None)
|
||||
for first_uid, first_seq in first_cal_events.items():
|
||||
if first_uid not in second_cal_events:
|
||||
return False
|
||||
second_seq = second_cal_events.get(first_uid, None)
|
||||
if first_seq != second_seq:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
return is_icals_equal_line_by_line(first, second)
|
||||
|
||||
|
||||
def ical_date_to_datetime(date, tz, start):
|
||||
|
|
@ -578,7 +577,7 @@ def list_of_gaps_in_schedule(schedule, start_date, end_date):
|
|||
end_datetime,
|
||||
)
|
||||
for event in events:
|
||||
start, end = start_end_with_respect_to_all_day(event, calendar_tz)
|
||||
start, end, _ = event_start_end_all_day_with_respect_to_type(event, calendar_tz)
|
||||
intervals.append(DatetimeInterval(start, end))
|
||||
return detect_gaps(intervals, start_datetime, end_datetime)
|
||||
|
||||
|
|
@ -615,6 +614,16 @@ def start_end_with_respect_to_all_day(event, calendar_tz):
|
|||
return start, end
|
||||
|
||||
|
||||
def event_start_end_all_day_with_respect_to_type(event, calendar_tz):
|
||||
all_day = False
|
||||
if type(event[ICAL_DATETIME_START].dt) == datetime.date:
|
||||
start, end = start_end_with_respect_to_all_day(event, calendar_tz)
|
||||
all_day = True
|
||||
else:
|
||||
start, end = ical_events.get_start_and_end_with_respect_to_event_type(event)
|
||||
return start, end, all_day
|
||||
|
||||
|
||||
def convert_windows_timezone_to_iana(tz_name):
|
||||
"""
|
||||
Conversion info taken from https://raw.githubusercontent.com/unicode-org/cldr/main/common/supplemental/windowsZones.xml
|
||||
|
|
|
|||
|
|
@ -676,6 +676,9 @@ class OnCallScheduleWeb(OnCallSchedule):
|
|||
pass
|
||||
else:
|
||||
if update_shift.event_is_started:
|
||||
custom_shift.rotation_start = max(
|
||||
custom_shift.rotation_start, timezone.now().replace(microsecond=0)
|
||||
)
|
||||
custom_shift.start_rotation_from_user_index = update_shift.start_rotation_from_user_index
|
||||
update_shift.until = custom_shift.rotation_start
|
||||
extra_shifts.append(update_shift)
|
||||
|
|
@ -692,7 +695,9 @@ class OnCallScheduleWeb(OnCallSchedule):
|
|||
|
||||
# filter events using a temporal overriden calendar including the not-yet-saved shift
|
||||
events = self.filter_events(user_tz, starting_date, days=days, with_empty=True, with_gap=True)
|
||||
shift_events = [e for e in events if e["shift"]["pk"] == custom_shift.public_primary_key]
|
||||
# return preview events for affected shifts
|
||||
updated_shift_pks = {s.public_primary_key for s in extra_shifts}
|
||||
shift_events = [e for e in events if e["shift"]["pk"] in updated_shift_pks]
|
||||
final_events = self._resolve_schedule(events)
|
||||
|
||||
_invalidate_cache(self, ical_property)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ def refresh_ical_file(schedule_pk):
|
|||
task_logger.info(f"run_task_primary {schedule_pk} {run_task_primary} prev_ical_file_primary is None")
|
||||
else:
|
||||
run_task_primary = not is_icals_equal(
|
||||
schedule.cached_ical_file_primary, schedule.prev_ical_file_primary, schedule
|
||||
schedule.cached_ical_file_primary,
|
||||
schedule.prev_ical_file_primary,
|
||||
)
|
||||
task_logger.info(f"run_task_primary {schedule_pk} {run_task_primary} icals not equal")
|
||||
run_task_overrides = False
|
||||
|
|
@ -57,7 +58,8 @@ def refresh_ical_file(schedule_pk):
|
|||
task_logger.info(f"run_task_overrides {schedule_pk} {run_task_primary} prev_ical_file_overrides is None")
|
||||
else:
|
||||
run_task_overrides = not is_icals_equal(
|
||||
schedule.cached_ical_file_overrides, schedule.prev_ical_file_overrides, schedule
|
||||
schedule.cached_ical_file_overrides,
|
||||
schedule.prev_ical_file_overrides,
|
||||
)
|
||||
task_logger.info(f"run_task_overrides {schedule_pk} {run_task_primary} icals not equal")
|
||||
run_task = run_task_primary or run_task_overrides
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue