Refactor gaps and empty shift checks (#3785)
Refactor gaps and empty shift checks: - Increase checking gaps and empty shifts frequency - Unify gaps and empty shift checks
This commit is contained in:
parent
801f1ad028
commit
16ce0136f3
18 changed files with 353 additions and 96 deletions
|
|
@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Changed
|
||||
|
||||
- Remove `/oncall` Slack slash command (ie. manual alert group creation command) by @joeyorlando ([#3790](https://github.com/grafana/oncall/pull/3790))
|
||||
- Increase frequency of checking for gaps and empty shifts in schedules by @Ferril ([#3785](https://github.com/grafana/oncall/pull/3785))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -88,9 +88,8 @@ class ScheduleBaseSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
|
||||
def create(self, validated_data):
|
||||
created_schedule = super().create(validated_data)
|
||||
created_schedule.check_empty_shifts_for_next_week()
|
||||
created_schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule_notify_about_empty_shifts_in_schedule.apply_async((created_schedule.pk,))
|
||||
created_schedule.check_gaps_for_next_week()
|
||||
schedule_notify_about_gaps_in_schedule.apply_async((created_schedule.pk,))
|
||||
return created_schedule
|
||||
|
||||
|
|
|
|||
|
|
@ -57,8 +57,7 @@ class ScheduleCalendarCreateSerializer(ScheduleCalendarSerializer):
|
|||
or old_enable_web_overrides != updated_enable_web_overrides
|
||||
):
|
||||
updated_schedule.drop_cached_ical()
|
||||
updated_schedule.check_empty_shifts_for_next_week()
|
||||
updated_schedule.check_gaps_for_next_week()
|
||||
updated_schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule_notify_about_empty_shifts_in_schedule.apply_async((instance.pk,))
|
||||
schedule_notify_about_gaps_in_schedule.apply_async((instance.pk,))
|
||||
return updated_schedule
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ class ScheduleICalUpdateSerializer(ScheduleICalCreateSerializer):
|
|||
|
||||
if old_ical_url_primary != updated_ical_url_primary or old_ical_url_overrides != updated_ical_url_overrides:
|
||||
updated_schedule.drop_cached_ical()
|
||||
updated_schedule.check_empty_shifts_for_next_week()
|
||||
updated_schedule.check_gaps_for_next_week()
|
||||
updated_schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule_notify_about_empty_shifts_in_schedule.apply_async((instance.pk,))
|
||||
schedule_notify_about_gaps_in_schedule.apply_async((instance.pk,))
|
||||
# for iCal-based schedules we need to refresh final schedule information
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ class ScheduleWebCreateSerializer(ScheduleWebSerializer):
|
|||
updated_time_zone = updated_schedule.time_zone
|
||||
if old_time_zone != updated_time_zone:
|
||||
updated_schedule.drop_cached_ical()
|
||||
updated_schedule.check_empty_shifts_for_next_week()
|
||||
updated_schedule.check_gaps_for_next_week()
|
||||
updated_schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule_notify_about_empty_shifts_in_schedule.apply_async((instance.pk,))
|
||||
schedule_notify_about_gaps_in_schedule.apply_async((instance.pk,))
|
||||
return updated_schedule
|
||||
|
|
|
|||
|
|
@ -455,8 +455,7 @@ class ScheduleView(
|
|||
def reload_ical(self, request, pk):
|
||||
schedule = self.get_object(annotate=False)
|
||||
schedule.drop_cached_ical()
|
||||
schedule.check_empty_shifts_for_next_week()
|
||||
schedule.check_gaps_for_next_week()
|
||||
schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
|
||||
if schedule.user_group is not None:
|
||||
update_slack_user_group_for_schedules.apply_async((schedule.user_group.pk,))
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from django.utils.functional import cached_property
|
|||
from icalendar.cal import Event
|
||||
|
||||
from apps.schedules.tasks import (
|
||||
check_gaps_and_empty_shifts_in_schedule,
|
||||
drop_cached_ical_task,
|
||||
refresh_ical_final_schedule,
|
||||
schedule_notify_about_empty_shifts_in_schedule,
|
||||
|
|
@ -692,6 +693,7 @@ class CustomOnCallShift(models.Model):
|
|||
schedule = self.schedule.get_real_instance()
|
||||
schedule.refresh_ical_file()
|
||||
refresh_ical_final_schedule.apply_async((schedule.pk,))
|
||||
check_gaps_and_empty_shifts_in_schedule.apply_async((schedule.pk,))
|
||||
|
||||
def start_drop_ical_and_check_schedule_tasks(self, schedule):
|
||||
drop_cached_ical_task.apply_async((schedule.pk,))
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from apps.schedules.constants import (
|
|||
ICAL_UID,
|
||||
)
|
||||
from apps.schedules.ical_utils import (
|
||||
EmptyShifts,
|
||||
create_base_icalendar,
|
||||
fetch_ical_file_or_get_error,
|
||||
get_oncall_users_for_multiple_schedules,
|
||||
|
|
@ -278,22 +279,34 @@ class OnCallSchedule(PolymorphicModel):
|
|||
(self.prev_ical_file_overrides, self.cached_ical_file_overrides),
|
||||
]
|
||||
|
||||
def check_gaps_for_next_week(self) -> bool:
|
||||
def check_gaps_and_empty_shifts_for_next_week(self) -> None:
|
||||
datetime_start = timezone.now()
|
||||
datetime_end = datetime_start + datetime.timedelta(days=7)
|
||||
|
||||
# get empty shifts from all events and gaps from final events
|
||||
events = self.filter_events(
|
||||
datetime_start,
|
||||
datetime_end,
|
||||
with_empty=True,
|
||||
with_gap=True,
|
||||
all_day_datetime=True,
|
||||
)
|
||||
has_empty_shifts = len([event for event in events if event["is_empty"]]) != 0
|
||||
final_events = self._resolve_schedule(events, datetime_start, datetime_end)
|
||||
has_gaps = len([final_event for final_event in final_events if final_event["is_gap"]]) != 0
|
||||
if has_gaps != self.has_gaps or has_empty_shifts != self.has_empty_shifts:
|
||||
self.has_gaps = has_gaps
|
||||
self.has_empty_shifts = has_empty_shifts
|
||||
self.save(update_fields=["has_gaps", "has_empty_shifts"])
|
||||
|
||||
def get_gaps_for_next_week(self) -> ScheduleEvents:
|
||||
today = timezone.now()
|
||||
events = self.final_events(today, today + datetime.timedelta(days=7))
|
||||
gaps = [event for event in events if event["is_gap"] and not event["is_empty"]]
|
||||
has_gaps = len(gaps) != 0
|
||||
self.has_gaps = has_gaps
|
||||
self.save(update_fields=["has_gaps"])
|
||||
return has_gaps
|
||||
return [event for event in events if event["is_gap"]]
|
||||
|
||||
def check_empty_shifts_for_next_week(self):
|
||||
def get_empty_shifts_for_next_week(self) -> EmptyShifts:
|
||||
today = timezone.now().date()
|
||||
empty_shifts = list_of_empty_shifts_in_schedule(self, today, today + datetime.timedelta(days=7))
|
||||
has_empty_shifts = len(empty_shifts) != 0
|
||||
self.has_empty_shifts = has_empty_shifts
|
||||
self.save(update_fields=["has_empty_shifts"])
|
||||
return has_empty_shifts
|
||||
return list_of_empty_shifts_in_schedule(self, today, today + datetime.timedelta(days=7))
|
||||
|
||||
def drop_cached_ical(self):
|
||||
self._drop_primary_ical_file()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from .check_gaps_and_empty_shifts import check_gaps_and_empty_shifts_in_schedule # noqa: F401
|
||||
from .drop_cached_ical import drop_cached_ical_for_custom_events_for_organization, drop_cached_ical_task # noqa: F401
|
||||
from .notify_about_empty_shifts_in_schedule import ( # noqa: F401
|
||||
check_empty_shifts_in_schedule,
|
||||
|
|
|
|||
23
engine/apps/schedules/tasks/check_gaps_and_empty_shifts.py
Normal file
23
engine/apps/schedules/tasks/check_gaps_and_empty_shifts.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from celery.utils.log import get_task_logger
|
||||
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
task_logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task()
|
||||
def check_gaps_and_empty_shifts_in_schedule(schedule_pk):
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
|
||||
task_logger.info(f"Start check_gaps_and_empty_shifts_in_schedule {schedule_pk}")
|
||||
|
||||
try:
|
||||
schedule = OnCallSchedule.objects.get(
|
||||
pk=schedule_pk,
|
||||
)
|
||||
except OnCallSchedule.DoesNotExist:
|
||||
task_logger.info(f"Tried to check_gaps_and_empty_shifts_in_schedule for non-existing schedule {schedule_pk}")
|
||||
return
|
||||
|
||||
schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
task_logger.info(f"Finish check_gaps_and_empty_shifts_in_schedule {schedule_pk}")
|
||||
|
|
@ -3,7 +3,6 @@ from celery.utils.log import get_task_logger
|
|||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.schedules.ical_utils import list_of_empty_shifts_in_schedule
|
||||
from apps.slack.utils import format_datetime_to_slack_with_time, post_message_to_channel
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
from common.utils import trim_if_needed
|
||||
|
|
@ -11,36 +10,16 @@ from common.utils import trim_if_needed
|
|||
task_logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
# deprecated # todo: delete this task from here and from task routes after the next release
|
||||
@shared_dedicated_queue_retry_task()
|
||||
def start_check_empty_shifts_in_schedule():
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
|
||||
task_logger.info("Start start_notify_about_empty_shifts_in_schedule")
|
||||
|
||||
schedules = OnCallSchedule.objects.all()
|
||||
|
||||
for schedule in schedules:
|
||||
check_empty_shifts_in_schedule.apply_async((schedule.pk,))
|
||||
|
||||
task_logger.info("Finish start_notify_about_empty_shifts_in_schedule")
|
||||
return
|
||||
|
||||
|
||||
# deprecated # todo: delete this task from here and from task routes after the next release
|
||||
@shared_dedicated_queue_retry_task()
|
||||
def check_empty_shifts_in_schedule(schedule_pk):
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
|
||||
task_logger.info(f"Start check_empty_shifts_in_schedule {schedule_pk}")
|
||||
|
||||
try:
|
||||
schedule = OnCallSchedule.objects.get(
|
||||
pk=schedule_pk,
|
||||
)
|
||||
except OnCallSchedule.DoesNotExist:
|
||||
task_logger.info(f"Tried to check_empty_shifts_in_schedule for non-existing schedule {schedule_pk}")
|
||||
return
|
||||
|
||||
schedule.check_empty_shifts_for_next_week()
|
||||
task_logger.info(f"Finish check_empty_shifts_in_schedule {schedule_pk}")
|
||||
return
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task()
|
||||
|
|
@ -54,6 +33,7 @@ def start_notify_about_empty_shifts_in_schedule():
|
|||
schedules = OnCallScheduleICal.objects.filter(
|
||||
empty_shifts_report_sent_at__lte=week_ago,
|
||||
channel__isnull=False,
|
||||
organization__deleted_at__isnull=True,
|
||||
)
|
||||
|
||||
for schedule in schedules:
|
||||
|
|
@ -79,9 +59,8 @@ def notify_about_empty_shifts_in_schedule_task(schedule_pk):
|
|||
task_logger.info(f"Tried to notify_about_empty_shifts_in_schedule_task for non-existing schedule {schedule_pk}")
|
||||
return
|
||||
|
||||
today = timezone.now().date()
|
||||
empty_shifts = list_of_empty_shifts_in_schedule(schedule, today, today + timezone.timedelta(days=7))
|
||||
schedule.empty_shifts_report_sent_at = today
|
||||
empty_shifts = schedule.get_empty_shifts_for_next_week()
|
||||
schedule.empty_shifts_report_sent_at = timezone.now().date()
|
||||
|
||||
if len(empty_shifts) != 0:
|
||||
schedule.has_empty_shifts = True
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import datetime
|
||||
|
||||
import pytz
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.core.cache import cache
|
||||
|
|
@ -11,36 +9,16 @@ from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
|||
task_logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
# deprecated # todo: delete this task from here and from task routes after the next release
|
||||
@shared_dedicated_queue_retry_task()
|
||||
def start_check_gaps_in_schedule():
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
|
||||
task_logger.info("Start start_check_gaps_in_schedule")
|
||||
|
||||
schedules = OnCallSchedule.objects.all()
|
||||
|
||||
for schedule in schedules:
|
||||
check_gaps_in_schedule.apply_async((schedule.pk,))
|
||||
|
||||
task_logger.info("Finish start_check_gaps_in_schedule")
|
||||
return
|
||||
|
||||
|
||||
# deprecated # todo: delete this task from here and from task routes after the next release
|
||||
@shared_dedicated_queue_retry_task()
|
||||
def check_gaps_in_schedule(schedule_pk):
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
|
||||
task_logger.info(f"Start check_gaps_in_schedule {schedule_pk}")
|
||||
|
||||
try:
|
||||
schedule = OnCallSchedule.objects.get(
|
||||
pk=schedule_pk,
|
||||
)
|
||||
except OnCallSchedule.DoesNotExist:
|
||||
task_logger.info(f"Tried to check_gaps_in_schedule for non-existing schedule {schedule_pk}")
|
||||
return
|
||||
|
||||
schedule.check_gaps_for_next_week()
|
||||
task_logger.info(f"Finish check_gaps_in_schedule {schedule_pk}")
|
||||
return
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task()
|
||||
|
|
@ -54,6 +32,7 @@ def start_notify_about_gaps_in_schedule():
|
|||
schedules = OnCallSchedule.objects.filter(
|
||||
gaps_report_sent_at__lte=week_ago,
|
||||
channel__isnull=False,
|
||||
organization__deleted_at__isnull=True,
|
||||
)
|
||||
|
||||
for schedule in schedules:
|
||||
|
|
@ -80,10 +59,8 @@ def notify_about_gaps_in_schedule_task(schedule_pk):
|
|||
task_logger.info(f"Tried to notify_about_gaps_in_schedule_task for non-existing schedule {schedule_pk}")
|
||||
return
|
||||
|
||||
now = timezone.now()
|
||||
events = schedule.final_events(now, now + datetime.timedelta(days=7))
|
||||
gaps = [event for event in events if event["is_gap"] and not event["is_empty"]]
|
||||
schedule.gaps_report_sent_at = now.date()
|
||||
gaps = schedule.get_gaps_for_next_week()
|
||||
schedule.gaps_report_sent_at = timezone.now().date()
|
||||
|
||||
if len(gaps) != 0:
|
||||
schedule.has_gaps = True
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ from celery.utils.log import get_task_logger
|
|||
|
||||
from apps.alerts.tasks import notify_ical_schedule_shift # type: ignore[no-redef]
|
||||
from apps.schedules.ical_utils import is_icals_equal, update_cached_oncall_users_for_schedule
|
||||
from apps.schedules.tasks import notify_about_empty_shifts_in_schedule_task, notify_about_gaps_in_schedule_task
|
||||
from apps.schedules.tasks import (
|
||||
check_gaps_and_empty_shifts_in_schedule,
|
||||
notify_about_empty_shifts_in_schedule_task,
|
||||
notify_about_gaps_in_schedule_task,
|
||||
)
|
||||
from apps.slack.tasks import start_update_slack_user_group_for_schedules
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
|
|
@ -84,6 +88,9 @@ def refresh_ical_file(schedule_pk):
|
|||
# update cached schedule on-call users
|
||||
update_cached_oncall_users_for_schedule(schedule)
|
||||
|
||||
check_gaps_and_empty_shifts_in_schedule.apply_async((schedule_pk,))
|
||||
# todo: refactor tasks below to unify checking and notifying about gaps and empty shifts to avoid doing the same
|
||||
# todo: work twice.
|
||||
if run_task:
|
||||
notify_about_empty_shifts_in_schedule_task.apply_async((schedule_pk,))
|
||||
notify_about_gaps_in_schedule_task.apply_async((schedule_pk,))
|
||||
|
|
|
|||
269
engine/apps/schedules/tests/test_check_gaps_and_empty_shifts.py
Normal file
269
engine/apps/schedules/tests/test_check_gaps_and_empty_shifts.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_no_empty_shifts_no_gaps(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1")
|
||||
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, name="test_schedule")
|
||||
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now - datetime.timedelta(days=7, minutes=1)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": datetime.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
schedule.refresh_ical_file()
|
||||
schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule.refresh_from_db()
|
||||
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_no_empty_shifts_but_gaps_now(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1")
|
||||
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, name="test_schedule")
|
||||
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now - datetime.timedelta(days=1, minutes=1)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": datetime.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
"interval": 2,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
schedule.refresh_ical_file()
|
||||
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule.refresh_from_db()
|
||||
|
||||
assert schedule.has_gaps is True
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_empty_shifts_no_gaps(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1", role=LegacyAccessControlRole.VIEWER)
|
||||
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, name="test_schedule")
|
||||
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now - datetime.timedelta(days=7, minutes=1)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": datetime.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
schedule.refresh_ical_file()
|
||||
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule.refresh_from_db()
|
||||
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_empty_shifts_and_gaps(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1", role=LegacyAccessControlRole.VIEWER)
|
||||
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, name="test_schedule")
|
||||
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now - datetime.timedelta(days=7, minutes=1)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": datetime.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
"interval": 2,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
schedule.refresh_ical_file()
|
||||
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule.refresh_from_db()
|
||||
|
||||
assert schedule.has_gaps is True
|
||||
assert schedule.has_empty_shifts is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_empty_shifts_and_gaps_in_the_past(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1", role=LegacyAccessControlRole.VIEWER)
|
||||
user2 = make_user(organization=organization, username="user2", role=LegacyAccessControlRole.ADMIN)
|
||||
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, name="test_schedule")
|
||||
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now - datetime.timedelta(days=7, minutes=1)
|
||||
until = start_date + datetime.timedelta(days=5, minutes=1)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": datetime.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
"interval": 2,
|
||||
"until": until,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
|
||||
start_date2 = now - datetime.timedelta(days=4, minutes=1)
|
||||
data2 = {
|
||||
"start": start_date2,
|
||||
"rotation_start": start_date2,
|
||||
"duration": datetime.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
}
|
||||
on_call_shift2 = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data2
|
||||
)
|
||||
on_call_shift2.add_rolling_users([[user2]])
|
||||
schedule.refresh_ical_file()
|
||||
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule.refresh_from_db()
|
||||
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_empty_shifts_and_gaps_in_the_future(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1", role=LegacyAccessControlRole.VIEWER)
|
||||
user2 = make_user(organization=organization, username="user2", role=LegacyAccessControlRole.ADMIN)
|
||||
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, name="test_schedule")
|
||||
# empty shift with gaps starts in 7 days 1 min
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now + datetime.timedelta(days=7, minutes=1)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": datetime.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
"interval": 2,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
# normal shift ends in 7 days 1 min
|
||||
start_date2 = now - datetime.timedelta(days=7, minutes=1)
|
||||
until = now + datetime.timedelta(days=7, minutes=1)
|
||||
data2 = {
|
||||
"start": start_date2,
|
||||
"rotation_start": start_date2,
|
||||
"duration": datetime.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
"until": until,
|
||||
}
|
||||
on_call_shift2 = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data2
|
||||
)
|
||||
on_call_shift2.add_rolling_users([[user2]])
|
||||
schedule.refresh_ical_file()
|
||||
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
schedule.check_gaps_and_empty_shifts_for_next_week()
|
||||
schedule.refresh_from_db()
|
||||
# no gaps and empty shifts in the next 7 days
|
||||
assert schedule.has_gaps is False
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
|
@ -53,6 +53,7 @@ def test_no_empty_shifts_no_triggering_notification(
|
|||
|
||||
schedule.refresh_from_db()
|
||||
assert empty_shifts_report_sent_at != schedule.empty_shifts_report_sent_at
|
||||
assert schedule.has_empty_shifts is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ def test_no_gaps_no_triggering_notification(
|
|||
|
||||
schedule.refresh_from_db()
|
||||
assert gaps_report_sent_at != schedule.gaps_report_sent_at
|
||||
assert schedule.check_gaps_for_next_week() is False
|
||||
assert schedule.has_gaps is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -115,7 +115,7 @@ def test_gaps_in_the_past_no_triggering_notification(
|
|||
|
||||
schedule.refresh_from_db()
|
||||
assert gaps_report_sent_at != schedule.gaps_report_sent_at
|
||||
assert schedule.check_gaps_for_next_week() is False
|
||||
assert schedule.has_gaps is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -166,7 +166,6 @@ def test_gaps_now_trigger_notification(
|
|||
schedule.refresh_from_db()
|
||||
assert gaps_report_sent_at != schedule.gaps_report_sent_at
|
||||
assert schedule.has_gaps is True
|
||||
assert schedule.check_gaps_for_next_week() is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -218,7 +217,6 @@ def test_gaps_near_future_trigger_notification(
|
|||
schedule.refresh_from_db()
|
||||
assert gaps_report_sent_at != schedule.gaps_report_sent_at
|
||||
assert schedule.has_gaps is True
|
||||
assert schedule.check_gaps_for_next_week() is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -267,4 +265,4 @@ def test_gaps_later_than_7_days_no_triggering_notification(
|
|||
|
||||
schedule.refresh_from_db()
|
||||
assert gaps_report_sent_at != schedule.gaps_report_sent_at
|
||||
assert schedule.check_gaps_for_next_week() is False
|
||||
assert schedule.has_gaps is False
|
||||
|
|
|
|||
|
|
@ -506,21 +506,11 @@ CELERY_BEAT_SCHEDULE = {
|
|||
"schedule": crontab(minute=1, hour=12, day_of_week="monday"),
|
||||
"args": (),
|
||||
},
|
||||
"start_check_gaps_in_schedule": {
|
||||
"task": "apps.schedules.tasks.notify_about_gaps_in_schedule.start_check_gaps_in_schedule",
|
||||
"schedule": crontab(minute=0, hour=0),
|
||||
"args": (),
|
||||
},
|
||||
"start_notify_about_empty_shifts_in_schedule": {
|
||||
"task": "apps.schedules.tasks.notify_about_empty_shifts_in_schedule.start_notify_about_empty_shifts_in_schedule",
|
||||
"schedule": crontab(minute=0, hour=12, day_of_week="monday"),
|
||||
"args": (),
|
||||
},
|
||||
"start_check_empty_shifts_in_schedule": {
|
||||
"task": "apps.schedules.tasks.notify_about_empty_shifts_in_schedule.start_check_empty_shifts_in_schedule",
|
||||
"schedule": crontab(minute=0, hour=0),
|
||||
"args": (),
|
||||
},
|
||||
"populate_slack_usergroups": {
|
||||
"task": "apps.slack.tasks.populate_slack_usergroups",
|
||||
"schedule": crontab(minute=0, hour=9, day_of_week="monday,wednesday,friday"),
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ CELERY_TASK_ROUTES = {
|
|||
"apps.schedules.tasks.refresh_ical_files.start_refresh_ical_files": {"queue": "default"},
|
||||
"apps.schedules.tasks.refresh_ical_files.refresh_ical_final_schedule": {"queue": "default"},
|
||||
"apps.schedules.tasks.refresh_ical_files.start_refresh_ical_final_schedules": {"queue": "default"},
|
||||
"apps.schedules.tasks.check_gaps_and_empty_shifts.check_gaps_and_empty_shifts_in_schedule": {"queue": "default"},
|
||||
"apps.schedules.tasks.notify_about_gaps_in_schedule.check_empty_shifts_in_schedule": {"queue": "default"},
|
||||
"apps.schedules.tasks.notify_about_gaps_in_schedule.start_notify_about_gaps_in_schedule": {"queue": "default"},
|
||||
"apps.schedules.tasks.notify_about_gaps_in_schedule.check_gaps_in_schedule": {"queue": "default"},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue