24
24
from apps .auth_token .auth import PluginAuthentication
25
25
from apps .auth_token .constants import SCHEDULE_EXPORT_TOKEN_NAME
26
26
from apps .auth_token .models import ScheduleExportAuthToken
27
- from apps .schedules .ical_utils import list_of_oncall_shifts_from_ical
28
27
from apps .schedules .models import OnCallSchedule
29
28
from apps .slack .models import SlackChannel
30
29
from apps .slack .tasks import update_slack_user_group_for_schedules
@@ -195,51 +194,14 @@ def get_request_timezone(self):
195
194
196
195
return user_tz , date
197
196
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
-
235
197
@action (detail = True , methods = ["get" ])
236
198
def events (self , request , pk ):
237
199
user_tz , date = self .get_request_timezone ()
238
200
with_empty = self .request .query_params .get ("with_empty" , False ) == "true"
239
201
with_gap = self .request .query_params .get ("with_gap" , False ) == "true"
240
202
241
203
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 )
243
205
244
206
slack_channel = (
245
207
{
@@ -281,16 +243,14 @@ def filter_events(self, request, pk):
281
243
raise BadRequest (detail = "Invalid days format" )
282
244
283
245
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
- )
287
246
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 )
294
254
295
255
result = {
296
256
"id" : schedule .public_primary_key ,
@@ -300,112 +260,14 @@ def filter_events(self, request, pk):
300
260
}
301
261
return Response (result , status = status .HTTP_200_OK )
302
262
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
-
400
263
@action (detail = True , methods = ["get" ])
401
264
def next_shifts_per_user (self , request , pk ):
402
265
"""Return next shift for users in schedule."""
403
266
user_tz , _ = self .get_request_timezone ()
404
267
now = timezone .now ()
405
268
starting_date = now .date ()
406
269
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 )
409
271
410
272
users = {}
411
273
for e in events :
0 commit comments