Skip to content

Commit 8a27168

Browse files
authored
Merge branch 'dev' into dev
2 parents 8bcedb4 + f3f7c17 commit 8a27168

9 files changed

+175
-47
lines changed

engine/apps/schedules/ical_utils.py

+13
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@
5454
logger.setLevel(logging.DEBUG)
5555

5656

57+
class MissingUser:
58+
"""Represent a missing user in a rolling users shift."""
59+
60+
DISPLAY_NAME = "(missing)"
61+
62+
def __init__(self, pk):
63+
self.pk = pk
64+
65+
@property
66+
def username(self):
67+
return self.DISPLAY_NAME
68+
69+
5770
EmptyShift = namedtuple(
5871
"EmptyShift",
5972
["start", "end", "summary", "description", "attendee", "all_day", "calendar_type", "calendar_tz", "shift_pk"],

engine/apps/schedules/models/custom_on_call_shift.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from django.utils.functional import cached_property
1919
from icalendar.cal import Event
2020

21+
from apps.schedules.ical_utils import MissingUser
2122
from apps.schedules.tasks import (
2223
check_gaps_and_empty_shifts_in_schedule,
2324
drop_cached_ical_task,
@@ -645,10 +646,6 @@ def get_rolling_users(self):
645646
all_users_pks = set()
646647
users_queue = []
647648
if self.rolling_users is not None:
648-
# get all users pks from rolling_users field
649-
for users_dict in self.rolling_users:
650-
all_users_pks.update(users_dict.keys())
651-
users = User.objects.filter(pk__in=all_users_pks)
652649
# generate users_queue list with user objects
653650
if self.start_rotation_from_user_index is not None:
654651
rolling_users = (
@@ -657,10 +654,22 @@ def get_rolling_users(self):
657654
)
658655
else:
659656
rolling_users = self.rolling_users
657+
658+
# get all users pks from rolling_users field
659+
for users_dict in self.rolling_users:
660+
all_users_pks.update(users_dict.keys())
661+
users = User.objects.filter(pk__in=all_users_pks)
662+
users_by_id = {user.pk: user for user in users}
660663
for users_dict in rolling_users:
661-
users_list = list(users.filter(pk__in=users_dict.keys()))
662-
if users_list:
663-
users_queue.append(users_list)
664+
users_list = []
665+
for user_pk in users_dict.keys():
666+
try:
667+
user_pk = int(user_pk)
668+
users_list.append(users_by_id.get(user_pk, MissingUser(user_pk)))
669+
except ValueError:
670+
users_list.append(MissingUser(user_pk))
671+
users_queue.append(users_list)
672+
664673
return users_queue
665674

666675
def add_rolling_users(self, rolling_users_list):

engine/apps/schedules/tasks/__init__.py

-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
from .check_gaps_and_empty_shifts import check_gaps_and_empty_shifts_in_schedule # noqa: F401
22
from .drop_cached_ical import drop_cached_ical_for_custom_events_for_organization, drop_cached_ical_task # noqa: F401
33
from .notify_about_empty_shifts_in_schedule import ( # noqa: F401
4-
check_empty_shifts_in_schedule,
54
notify_about_empty_shifts_in_schedule_task,
65
schedule_notify_about_empty_shifts_in_schedule,
7-
start_check_empty_shifts_in_schedule,
86
start_notify_about_empty_shifts_in_schedule,
97
)
108
from .notify_about_gaps_in_schedule import ( # noqa: F401
11-
check_gaps_in_schedule,
129
notify_about_gaps_in_schedule_task,
1310
schedule_notify_about_gaps_in_schedule,
14-
start_check_gaps_in_schedule,
1511
start_notify_about_gaps_in_schedule,
1612
)
1713
from .refresh_ical_files import ( # noqa: F401

engine/apps/schedules/tasks/notify_about_empty_shifts_in_schedule.py

+4-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytz
22
from celery.utils.log import get_task_logger
33
from django.core.cache import cache
4+
from django.db.models import Q
45
from django.utils import timezone
56

67
from apps.slack.utils import format_datetime_to_slack_with_time, post_message_to_channel
@@ -10,28 +11,16 @@
1011
task_logger = get_task_logger(__name__)
1112

1213

13-
# deprecated # todo: delete this task from here and from task routes after the next release
14-
@shared_dedicated_queue_retry_task()
15-
def start_check_empty_shifts_in_schedule():
16-
return
17-
18-
19-
# deprecated # todo: delete this task from here and from task routes after the next release
20-
@shared_dedicated_queue_retry_task()
21-
def check_empty_shifts_in_schedule(schedule_pk):
22-
return
23-
24-
2514
@shared_dedicated_queue_retry_task()
2615
def start_notify_about_empty_shifts_in_schedule():
27-
from apps.schedules.models import OnCallScheduleICal
16+
from apps.schedules.models import OnCallSchedule
2817

2918
task_logger.info("Start start_notify_about_empty_shifts_in_schedule")
3019

3120
today = timezone.now().date()
3221
week_ago = today - timezone.timedelta(days=7)
33-
schedules = OnCallScheduleICal.objects.filter(
34-
empty_shifts_report_sent_at__lte=week_ago,
22+
schedules = OnCallSchedule.objects.filter(
23+
Q(empty_shifts_report_sent_at__lte=week_ago) | Q(empty_shifts_report_sent_at__isnull=True),
3524
slack_channel__isnull=False,
3625
organization__deleted_at__isnull=True,
3726
)

engine/apps/schedules/tasks/notify_about_gaps_in_schedule.py

+2-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytz
22
from celery.utils.log import get_task_logger
33
from django.core.cache import cache
4+
from django.db.models import Q
45
from django.utils import timezone
56

67
from apps.slack.utils import format_datetime_to_slack_with_time, post_message_to_channel
@@ -9,18 +10,6 @@
910
task_logger = get_task_logger(__name__)
1011

1112

12-
# deprecated # todo: delete this task from here and from task routes after the next release
13-
@shared_dedicated_queue_retry_task()
14-
def start_check_gaps_in_schedule():
15-
return
16-
17-
18-
# deprecated # todo: delete this task from here and from task routes after the next release
19-
@shared_dedicated_queue_retry_task()
20-
def check_gaps_in_schedule(schedule_pk):
21-
return
22-
23-
2413
@shared_dedicated_queue_retry_task()
2514
def start_notify_about_gaps_in_schedule():
2615
from apps.schedules.models import OnCallSchedule
@@ -30,7 +19,7 @@ def start_notify_about_gaps_in_schedule():
3019
today = timezone.now().date()
3120
week_ago = today - timezone.timedelta(days=7)
3221
schedules = OnCallSchedule.objects.filter(
33-
gaps_report_sent_at__lte=week_ago,
22+
Q(gaps_report_sent_at__lte=week_ago) | Q(gaps_report_sent_at__isnull=True),
3423
slack_channel__isnull=False,
3524
organization__deleted_at__isnull=True,
3625
)

engine/apps/schedules/tests/test_notify_about_empty_shifts_in_schedule.py

+44-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from django.utils import timezone
66

77
from apps.api.permissions import LegacyAccessControlRole
8-
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb
9-
from apps.schedules.tasks import notify_about_empty_shifts_in_schedule_task
8+
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar, OnCallScheduleICal, OnCallScheduleWeb
9+
from apps.schedules.tasks import notify_about_empty_shifts_in_schedule_task, start_notify_about_empty_shifts_in_schedule
1010

1111

1212
@pytest.mark.django_db
@@ -174,3 +174,45 @@ def test_empty_non_empty_shifts_trigger_notification(
174174
schedule.refresh_from_db()
175175
assert empty_shifts_report_sent_at != schedule.empty_shifts_report_sent_at
176176
assert schedule.has_empty_shifts
177+
178+
179+
@pytest.mark.parametrize(
180+
"schedule_class",
181+
[OnCallScheduleWeb, OnCallScheduleICal, OnCallScheduleCalendar],
182+
)
183+
@pytest.mark.parametrize(
184+
"report_sent_days_ago,expected_call",
185+
[(8, True), (6, False), (None, True)],
186+
)
187+
@pytest.mark.django_db
188+
def test_start_notify_about_empty_shifts(
189+
make_slack_team_identity,
190+
make_slack_channel,
191+
make_organization,
192+
make_schedule,
193+
schedule_class,
194+
report_sent_days_ago,
195+
expected_call,
196+
):
197+
slack_team_identity = make_slack_team_identity()
198+
slack_channel = make_slack_channel(slack_team_identity)
199+
organization = make_organization(slack_team_identity=slack_team_identity)
200+
201+
sent = timezone.now() - datetime.timedelta(days=report_sent_days_ago) if report_sent_days_ago else None
202+
schedule = make_schedule(
203+
organization,
204+
schedule_class=schedule_class,
205+
name="test_schedule",
206+
slack_channel=slack_channel,
207+
empty_shifts_report_sent_at=sent,
208+
)
209+
210+
with patch(
211+
"apps.schedules.tasks.notify_about_empty_shifts_in_schedule.notify_about_empty_shifts_in_schedule_task.apply_async"
212+
) as mock_notify:
213+
start_notify_about_empty_shifts_in_schedule()
214+
215+
if expected_call:
216+
mock_notify.assert_called_once_with((schedule.pk,))
217+
else:
218+
mock_notify.assert_not_called()

engine/apps/schedules/tests/test_notify_about_gaps_in_schedule.py

+44-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import pytest
55
from django.utils import timezone
66

7-
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb
8-
from apps.schedules.tasks import notify_about_gaps_in_schedule_task
7+
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar, OnCallScheduleICal, OnCallScheduleWeb
8+
from apps.schedules.tasks import notify_about_gaps_in_schedule_task, start_notify_about_gaps_in_schedule
99

1010

1111
@pytest.mark.django_db
@@ -286,3 +286,45 @@ def test_gaps_later_than_7_days_no_triggering_notification(
286286
schedule.refresh_from_db()
287287
assert gaps_report_sent_at != schedule.gaps_report_sent_at
288288
assert schedule.has_gaps is False
289+
290+
291+
@pytest.mark.parametrize(
292+
"schedule_class",
293+
[OnCallScheduleWeb, OnCallScheduleICal, OnCallScheduleCalendar],
294+
)
295+
@pytest.mark.parametrize(
296+
"report_sent_days_ago,expected_call",
297+
[(8, True), (6, False), (None, True)],
298+
)
299+
@pytest.mark.django_db
300+
def test_start_notify_about_gaps(
301+
make_slack_team_identity,
302+
make_slack_channel,
303+
make_organization,
304+
make_schedule,
305+
schedule_class,
306+
report_sent_days_ago,
307+
expected_call,
308+
):
309+
slack_team_identity = make_slack_team_identity()
310+
slack_channel = make_slack_channel(slack_team_identity)
311+
organization = make_organization(slack_team_identity=slack_team_identity)
312+
313+
sent = timezone.now() - datetime.timedelta(days=report_sent_days_ago) if report_sent_days_ago else None
314+
schedule = make_schedule(
315+
organization,
316+
schedule_class=schedule_class,
317+
name="test_schedule",
318+
slack_channel=slack_channel,
319+
gaps_report_sent_at=sent,
320+
)
321+
322+
with patch(
323+
"apps.schedules.tasks.notify_about_gaps_in_schedule.notify_about_gaps_in_schedule_task.apply_async"
324+
) as mock_notify:
325+
start_notify_about_gaps_in_schedule()
326+
327+
if expected_call:
328+
mock_notify.assert_called_once_with((schedule.pk,))
329+
else:
330+
mock_notify.assert_not_called()

engine/apps/schedules/tests/test_on_call_schedule.py

+52
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
ICAL_STATUS_CANCELLED,
1919
ICAL_SUMMARY,
2020
)
21+
from apps.schedules.ical_utils import MissingUser
2122
from apps.schedules.models import (
2223
CustomOnCallShift,
2324
OnCallSchedule,
@@ -358,6 +359,57 @@ def test_filter_events_include_empty(make_organization, make_user_for_organizati
358359
assert events == expected
359360

360361

362+
@pytest.mark.django_db
363+
def test_filter_events_include_empty_if_deleted(
364+
make_organization, make_user_for_organization, make_schedule, make_on_call_shift
365+
):
366+
organization = make_organization()
367+
schedule = make_schedule(
368+
organization,
369+
schedule_class=OnCallScheduleWeb,
370+
name="test_web_schedule",
371+
)
372+
user = make_user_for_organization(organization)
373+
now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
374+
start_date = now - timezone.timedelta(days=7)
375+
376+
data = {
377+
"start": start_date + timezone.timedelta(hours=10),
378+
"rotation_start": start_date + timezone.timedelta(hours=10),
379+
"duration": timezone.timedelta(hours=8),
380+
"priority_level": 1,
381+
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
382+
"schedule": schedule,
383+
}
384+
on_call_shift = make_on_call_shift(
385+
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
386+
)
387+
on_call_shift.add_rolling_users([[user]])
388+
389+
# user is deleted, shift data still exists but the shift is empty
390+
user.delete()
391+
392+
end_date = start_date + timezone.timedelta(days=1)
393+
events = schedule.filter_events(start_date, end_date, filter_by=OnCallSchedule.TYPE_ICAL_PRIMARY, with_empty=True)
394+
expected = [
395+
{
396+
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
397+
"start": on_call_shift.start,
398+
"end": on_call_shift.start + on_call_shift.duration,
399+
"all_day": False,
400+
"is_override": False,
401+
"is_empty": True,
402+
"is_gap": False,
403+
"priority_level": on_call_shift.priority_level,
404+
"missing_users": [MissingUser.DISPLAY_NAME],
405+
"users": [],
406+
"shift": {"pk": on_call_shift.public_primary_key},
407+
"source": "api",
408+
}
409+
]
410+
assert events == expected
411+
412+
361413
@pytest.mark.django_db
362414
def test_filter_events_ical_all_day(make_organization, make_user_for_organization, make_schedule, get_ical):
363415
calendar = get_ical("calendar_with_all_day_event.ics")

engine/settings/celery_task_routes.py

-4
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,9 @@
3333
"apps.schedules.tasks.refresh_ical_files.refresh_ical_final_schedule": {"queue": "default"},
3434
"apps.schedules.tasks.refresh_ical_files.start_refresh_ical_final_schedules": {"queue": "default"},
3535
"apps.schedules.tasks.check_gaps_and_empty_shifts.check_gaps_and_empty_shifts_in_schedule": {"queue": "default"},
36-
"apps.schedules.tasks.notify_about_gaps_in_schedule.check_empty_shifts_in_schedule": {"queue": "default"},
3736
"apps.schedules.tasks.notify_about_gaps_in_schedule.start_notify_about_gaps_in_schedule": {"queue": "default"},
38-
"apps.schedules.tasks.notify_about_gaps_in_schedule.check_gaps_in_schedule": {"queue": "default"},
3937
"apps.schedules.tasks.notify_about_gaps_in_schedule.notify_about_gaps_in_schedule_task": {"queue": "default"},
4038
"apps.schedules.tasks.notify_about_gaps_in_schedule.schedule_notify_about_gaps_in_schedule": {"queue": "default"},
41-
"apps.schedules.tasks.notify_about_gaps_in_schedule.start_check_empty_shifts_in_schedule": {"queue": "default"},
42-
"apps.schedules.tasks.notify_about_gaps_in_schedule.start_check_gaps_in_schedule": {"queue": "default"},
4339
"apps.schedules.tasks.notify_about_gaps_in_schedule.start_notify_about_empty_shifts_in_schedule": {
4440
"queue": "default"
4541
},

0 commit comments

Comments
 (0)