Skip to content

Commit 89f68d4

Browse files
authored
Merge pull request #353 from grafana/dev
Merge dev to main
2 parents 3d9542c + 83d5ccb commit 89f68d4

File tree

8 files changed

+494
-155
lines changed

8 files changed

+494
-155
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Change Log
22

3+
## v1.0.19 (2022-08-10)
4+
- Bug fixes
5+
36
## v1.0.15 (2022-08-03)
47
- Bug fixes
58

docs/sources/open-source.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ lt --port 8080 -s pretty-turkey-83 --print-requests
7979
type: message
8080
callback_id: incident_create
8181
description: Creates a new OnCall incident
82-
- name: Add to postmortem
82+
- name: Add to resolution note
8383
type: message
84-
callback_id: add_postmortem
85-
description: Add this message to postmortem
84+
callback_id: add_resolution_note
85+
description: Add this message to resolution note
8686
slash_commands:
8787
- command: /oncall
8888
url: <ONCALL_ENGINE_PUBLIC_URL>/slack/interactive_api_endpoint/

engine/apps/api/views/schedule.py

+9-147
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from apps.auth_token.auth import PluginAuthentication
2525
from apps.auth_token.constants import SCHEDULE_EXPORT_TOKEN_NAME
2626
from apps.auth_token.models import ScheduleExportAuthToken
27-
from apps.schedules.ical_utils import list_of_oncall_shifts_from_ical
2827
from apps.schedules.models import OnCallSchedule
2928
from apps.slack.models import SlackChannel
3029
from apps.slack.tasks import update_slack_user_group_for_schedules
@@ -195,51 +194,14 @@ def get_request_timezone(self):
195194

196195
return user_tz, date
197196

198-
def _filter_events(self, schedule, user_timezone, starting_date, days, with_empty, with_gap):
199-
shifts = (
200-
list_of_oncall_shifts_from_ical(schedule, starting_date, user_timezone, with_empty, with_gap, days=days)
201-
or []
202-
)
203-
events = []
204-
# for start, end, users, priority_level, source in shifts:
205-
for shift in shifts:
206-
all_day = type(shift["start"]) == datetime.date
207-
is_gap = shift.get("is_gap", False)
208-
shift_json = {
209-
"all_day": all_day,
210-
"start": shift["start"],
211-
# fix confusing end date for all-day event
212-
"end": shift["end"] - timezone.timedelta(days=1) if all_day else shift["end"],
213-
"users": [
214-
{
215-
"display_name": user.username,
216-
"pk": user.public_primary_key,
217-
}
218-
for user in shift["users"]
219-
],
220-
"missing_users": shift["missing_users"],
221-
"priority_level": shift["priority"] if shift["priority"] != 0 else None,
222-
"source": shift["source"],
223-
"calendar_type": shift["calendar_type"],
224-
"is_empty": len(shift["users"]) == 0 and not is_gap,
225-
"is_gap": is_gap,
226-
"is_override": shift["calendar_type"] == OnCallSchedule.TYPE_ICAL_OVERRIDES,
227-
"shift": {
228-
"pk": shift["shift_pk"],
229-
},
230-
}
231-
events.append(shift_json)
232-
233-
return events
234-
235197
@action(detail=True, methods=["get"])
236198
def events(self, request, pk):
237199
user_tz, date = self.get_request_timezone()
238200
with_empty = self.request.query_params.get("with_empty", False) == "true"
239201
with_gap = self.request.query_params.get("with_gap", False) == "true"
240202

241203
schedule = self.original_get_object()
242-
events = self._filter_events(schedule, user_tz, date, days=1, with_empty=with_empty, with_gap=with_gap)
204+
events = schedule.filter_events(user_tz, date, days=1, with_empty=with_empty, with_gap=with_gap)
243205

244206
slack_channel = (
245207
{
@@ -281,16 +243,14 @@ def filter_events(self, request, pk):
281243
raise BadRequest(detail="Invalid days format")
282244

283245
schedule = self.original_get_object()
284-
events = self._filter_events(
285-
schedule, user_tz, starting_date, days=days, with_empty=True, with_gap=resolve_schedule
286-
)
287246

288-
if filter_by == EVENTS_FILTER_BY_OVERRIDE:
289-
events = [e for e in events if e["calendar_type"] == OnCallSchedule.OVERRIDES]
290-
elif filter_by == EVENTS_FILTER_BY_ROTATION:
291-
events = [e for e in events if e["calendar_type"] == OnCallSchedule.PRIMARY]
292-
else: # resolve_schedule
293-
events = self._resolve_schedule(events)
247+
if filter_by is not None:
248+
filter_by = OnCallSchedule.PRIMARY if filter_by == EVENTS_FILTER_BY_ROTATION else OnCallSchedule.OVERRIDES
249+
events = schedule.filter_events(
250+
user_tz, starting_date, days=days, with_empty=True, with_gap=resolve_schedule, filter_by=filter_by
251+
)
252+
else: # return final schedule
253+
events = schedule.final_events(user_tz, starting_date, days)
294254

295255
result = {
296256
"id": schedule.public_primary_key,
@@ -300,112 +260,14 @@ def filter_events(self, request, pk):
300260
}
301261
return Response(result, status=status.HTTP_200_OK)
302262

303-
def _resolve_schedule(self, events):
304-
"""Calculate final schedule shifts considering rotations and overrides."""
305-
if not events:
306-
return []
307-
308-
# sort schedule events by (type desc, priority desc, start timestamp asc)
309-
events.sort(
310-
key=lambda e: (
311-
-e["calendar_type"] if e["calendar_type"] else 0, # overrides: 1, shifts: 0, gaps: None
312-
-e["priority_level"] if e["priority_level"] else 0,
313-
e["start"],
314-
)
315-
)
316-
317-
def _merge_intervals(evs):
318-
"""Keep track of scheduled intervals."""
319-
if not evs:
320-
return []
321-
intervals = [[e["start"], e["end"]] for e in evs]
322-
result = [intervals[0]]
323-
for interval in intervals[1:]:
324-
previous_interval = result[-1]
325-
if previous_interval[0] <= interval[0] <= previous_interval[1]:
326-
previous_interval[1] = max(previous_interval[1], interval[1])
327-
else:
328-
result.append(interval)
329-
return result
330-
331-
# iterate over events, reserving schedule slots based on their priority
332-
# if the expected slot was already scheduled for a higher priority event,
333-
# split the event, or fix start/end timestamps accordingly
334-
335-
# include overrides from start
336-
resolved = [e for e in events if e["calendar_type"] == OnCallSchedule.TYPE_ICAL_OVERRIDES]
337-
intervals = _merge_intervals(resolved)
338-
339-
pending = events[len(resolved) :]
340-
if not pending:
341-
return resolved
342-
343-
current_event_idx = 0 # current event to resolve
344-
current_interval_idx = 0 # current scheduled interval being checked
345-
current_priority = pending[0]["priority_level"] # current priority level being resolved
346-
347-
while current_event_idx < len(pending):
348-
ev = pending[current_event_idx]
349-
350-
if ev["priority_level"] != current_priority:
351-
# update scheduled intervals on priority change
352-
# and start from the beginning for the new priority level
353-
resolved.sort(key=lambda e: e["start"])
354-
intervals = _merge_intervals(resolved)
355-
current_interval_idx = 0
356-
current_priority = ev["priority_level"]
357-
358-
if current_interval_idx >= len(intervals):
359-
# event outside scheduled intervals, add to resolved
360-
resolved.append(ev)
361-
current_event_idx += 1
362-
elif ev["start"] < intervals[current_interval_idx][0] and ev["end"] <= intervals[current_interval_idx][0]:
363-
# event starts and ends outside an already scheduled interval, add to resolved
364-
resolved.append(ev)
365-
current_event_idx += 1
366-
elif ev["start"] < intervals[current_interval_idx][0] and ev["end"] > intervals[current_interval_idx][0]:
367-
# event starts outside interval but overlaps with an already scheduled interval
368-
# 1. add a split event copy to schedule the time before the already scheduled interval
369-
to_add = ev.copy()
370-
to_add["end"] = intervals[current_interval_idx][0]
371-
resolved.append(to_add)
372-
# 2. check if there is still time to be scheduled after the current scheduled interval ends
373-
if ev["end"] > intervals[current_interval_idx][1]:
374-
# event ends after current interval, update event start timestamp to match the interval end
375-
# and process the updated event as any other event
376-
ev["start"] = intervals[current_interval_idx][1]
377-
else:
378-
# done, go to next event
379-
current_event_idx += 1
380-
elif ev["start"] >= intervals[current_interval_idx][0] and ev["end"] <= intervals[current_interval_idx][1]:
381-
# event inside an already scheduled interval, ignore (go to next)
382-
current_event_idx += 1
383-
elif (
384-
ev["start"] >= intervals[current_interval_idx][0]
385-
and ev["start"] < intervals[current_interval_idx][1]
386-
and ev["end"] > intervals[current_interval_idx][1]
387-
):
388-
# event starts inside a scheduled interval but ends out of it
389-
# update the event start timestamp to match the interval end
390-
ev["start"] = intervals[current_interval_idx][1]
391-
# move to next interval and process the updated event as any other event
392-
current_interval_idx += 1
393-
elif ev["start"] >= intervals[current_interval_idx][1]:
394-
# event starts after the current interval, move to next interval and go through it
395-
current_interval_idx += 1
396-
397-
resolved.sort(key=lambda e: e["start"])
398-
return resolved
399-
400263
@action(detail=True, methods=["get"])
401264
def next_shifts_per_user(self, request, pk):
402265
"""Return next shift for users in schedule."""
403266
user_tz, _ = self.get_request_timezone()
404267
now = timezone.now()
405268
starting_date = now.date()
406269
schedule = self.original_get_object()
407-
shift_events = self._filter_events(schedule, user_tz, starting_date, days=30, with_empty=False, with_gap=False)
408-
events = self._resolve_schedule(shift_events)
270+
events = schedule.final_events(user_tz, starting_date, days=30)
409271

410272
users = {}
411273
for e in events:

engine/apps/schedules/ical_utils.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,13 @@ def memoized_users_in_ical(usernames_from_ical, organization):
8383

8484
# used for display schedule events on web
8585
def list_of_oncall_shifts_from_ical(
86-
schedule, date, user_timezone="UTC", with_empty_shifts=False, with_gaps=False, days=1
86+
schedule,
87+
date,
88+
user_timezone="UTC",
89+
with_empty_shifts=False,
90+
with_gaps=False,
91+
days=1,
92+
filter_by=None,
8793
):
8894
"""
8995
Parse the ical file and return list of events with users
@@ -122,6 +128,9 @@ def list_of_oncall_shifts_from_ical(
122128
else:
123129
calendar_type = OnCallSchedule.OVERRIDES
124130

131+
if filter_by is not None and filter_by != calendar_type:
132+
continue
133+
125134
tmp_result_datetime, tmp_result_date = get_shifts_dict(
126135
calendar, calendar_type, schedule, datetime_start, datetime_end, date, with_empty_shifts
127136
)

0 commit comments

Comments
 (0)