Merge pull request #353 from grafana/dev

Merge dev to main
This commit is contained in:
Matias Bordese 2022-08-10 13:19:18 -03:00 committed by GitHub
commit 89f68d45ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 494 additions and 155 deletions

View file

@ -1,5 +1,8 @@
# Change Log
## v1.0.19 (2022-08-10)
- Bug fixes
## v1.0.15 (2022-08-03)
- Bug fixes

View file

@ -79,10 +79,10 @@ lt --port 8080 -s pretty-turkey-83 --print-requests
type: message
callback_id: incident_create
description: Creates a new OnCall incident
- name: Add to postmortem
- name: Add to resolution note
type: message
callback_id: add_postmortem
description: Add this message to postmortem
callback_id: add_resolution_note
description: Add this message to resolution note
slash_commands:
- command: /oncall
url: <ONCALL_ENGINE_PUBLIC_URL>/slack/interactive_api_endpoint/

View file

@ -24,7 +24,6 @@ from apps.api.serializers.schedule_polymorphic import (
from apps.auth_token.auth import PluginAuthentication
from apps.auth_token.constants import SCHEDULE_EXPORT_TOKEN_NAME
from apps.auth_token.models import ScheduleExportAuthToken
from apps.schedules.ical_utils import list_of_oncall_shifts_from_ical
from apps.schedules.models import OnCallSchedule
from apps.slack.models import SlackChannel
from apps.slack.tasks import update_slack_user_group_for_schedules
@ -195,43 +194,6 @@ class ScheduleView(
return user_tz, date
def _filter_events(self, schedule, user_timezone, starting_date, days, with_empty, with_gap):
shifts = (
list_of_oncall_shifts_from_ical(schedule, starting_date, user_timezone, with_empty, with_gap, days=days)
or []
)
events = []
# for start, end, users, priority_level, source in shifts:
for shift in shifts:
all_day = type(shift["start"]) == datetime.date
is_gap = shift.get("is_gap", False)
shift_json = {
"all_day": all_day,
"start": shift["start"],
# fix confusing end date for all-day event
"end": shift["end"] - timezone.timedelta(days=1) if all_day else shift["end"],
"users": [
{
"display_name": user.username,
"pk": user.public_primary_key,
}
for user in shift["users"]
],
"missing_users": shift["missing_users"],
"priority_level": shift["priority"] if shift["priority"] != 0 else None,
"source": shift["source"],
"calendar_type": shift["calendar_type"],
"is_empty": len(shift["users"]) == 0 and not is_gap,
"is_gap": is_gap,
"is_override": shift["calendar_type"] == OnCallSchedule.TYPE_ICAL_OVERRIDES,
"shift": {
"pk": shift["shift_pk"],
},
}
events.append(shift_json)
return events
@action(detail=True, methods=["get"])
def events(self, request, pk):
user_tz, date = self.get_request_timezone()
@ -239,7 +201,7 @@ class ScheduleView(
with_gap = self.request.query_params.get("with_gap", False) == "true"
schedule = self.original_get_object()
events = self._filter_events(schedule, user_tz, date, days=1, with_empty=with_empty, with_gap=with_gap)
events = schedule.filter_events(user_tz, date, days=1, with_empty=with_empty, with_gap=with_gap)
slack_channel = (
{
@ -281,16 +243,14 @@ class ScheduleView(
raise BadRequest(detail="Invalid days format")
schedule = self.original_get_object()
events = self._filter_events(
schedule, user_tz, starting_date, days=days, with_empty=True, with_gap=resolve_schedule
)
if filter_by == EVENTS_FILTER_BY_OVERRIDE:
events = [e for e in events if e["calendar_type"] == OnCallSchedule.OVERRIDES]
elif filter_by == EVENTS_FILTER_BY_ROTATION:
events = [e for e in events if e["calendar_type"] == OnCallSchedule.PRIMARY]
else: # resolve_schedule
events = self._resolve_schedule(events)
if filter_by is not None:
filter_by = OnCallSchedule.PRIMARY if filter_by == EVENTS_FILTER_BY_ROTATION else OnCallSchedule.OVERRIDES
events = schedule.filter_events(
user_tz, starting_date, days=days, with_empty=True, with_gap=resolve_schedule, filter_by=filter_by
)
else: # return final schedule
events = schedule.final_events(user_tz, starting_date, days)
result = {
"id": schedule.public_primary_key,
@ -300,103 +260,6 @@ class ScheduleView(
}
return Response(result, status=status.HTTP_200_OK)
def _resolve_schedule(self, events):
"""Calculate final schedule shifts considering rotations and overrides."""
if not events:
return []
# sort schedule events by (type desc, priority desc, start timestamp asc)
events.sort(
key=lambda e: (
-e["calendar_type"] if e["calendar_type"] else 0, # overrides: 1, shifts: 0, gaps: None
-e["priority_level"] if e["priority_level"] else 0,
e["start"],
)
)
def _merge_intervals(evs):
"""Keep track of scheduled intervals."""
if not evs:
return []
intervals = [[e["start"], e["end"]] for e in evs]
result = [intervals[0]]
for interval in intervals[1:]:
previous_interval = result[-1]
if previous_interval[0] <= interval[0] <= previous_interval[1]:
previous_interval[1] = max(previous_interval[1], interval[1])
else:
result.append(interval)
return result
# iterate over events, reserving schedule slots based on their priority
# if the expected slot was already scheduled for a higher priority event,
# split the event, or fix start/end timestamps accordingly
# include overrides from start
resolved = [e for e in events if e["calendar_type"] == OnCallSchedule.TYPE_ICAL_OVERRIDES]
intervals = _merge_intervals(resolved)
pending = events[len(resolved) :]
if not pending:
return resolved
current_event_idx = 0 # current event to resolve
current_interval_idx = 0 # current scheduled interval being checked
current_priority = pending[0]["priority_level"] # current priority level being resolved
while current_event_idx < len(pending):
ev = pending[current_event_idx]
if ev["priority_level"] != current_priority:
# update scheduled intervals on priority change
# and start from the beginning for the new priority level
resolved.sort(key=lambda e: e["start"])
intervals = _merge_intervals(resolved)
current_interval_idx = 0
current_priority = ev["priority_level"]
if current_interval_idx >= len(intervals):
# event outside scheduled intervals, add to resolved
resolved.append(ev)
current_event_idx += 1
elif ev["start"] < intervals[current_interval_idx][0] and ev["end"] <= intervals[current_interval_idx][0]:
# event starts and ends outside an already scheduled interval, add to resolved
resolved.append(ev)
current_event_idx += 1
elif ev["start"] < intervals[current_interval_idx][0] and ev["end"] > intervals[current_interval_idx][0]:
# event starts outside interval but overlaps with an already scheduled interval
# 1. add a split event copy to schedule the time before the already scheduled interval
to_add = ev.copy()
to_add["end"] = intervals[current_interval_idx][0]
resolved.append(to_add)
# 2. check if there is still time to be scheduled after the current scheduled interval ends
if ev["end"] > intervals[current_interval_idx][1]:
# event ends after current interval, update event start timestamp to match the interval end
# and process the updated event as any other event
ev["start"] = intervals[current_interval_idx][1]
else:
# done, go to next event
current_event_idx += 1
elif ev["start"] >= intervals[current_interval_idx][0] and ev["end"] <= intervals[current_interval_idx][1]:
# event inside an already scheduled interval, ignore (go to next)
current_event_idx += 1
elif (
ev["start"] >= intervals[current_interval_idx][0]
and ev["start"] < intervals[current_interval_idx][1]
and ev["end"] > intervals[current_interval_idx][1]
):
# event starts inside a scheduled interval but ends out of it
# update the event start timestamp to match the interval end
ev["start"] = intervals[current_interval_idx][1]
# move to next interval and process the updated event as any other event
current_interval_idx += 1
elif ev["start"] >= intervals[current_interval_idx][1]:
# event starts after the current interval, move to next interval and go through it
current_interval_idx += 1
resolved.sort(key=lambda e: e["start"])
return resolved
@action(detail=True, methods=["get"])
def next_shifts_per_user(self, request, pk):
"""Return next shift for users in schedule."""
@ -404,8 +267,7 @@ class ScheduleView(
now = timezone.now()
starting_date = now.date()
schedule = self.original_get_object()
shift_events = self._filter_events(schedule, user_tz, starting_date, days=30, with_empty=False, with_gap=False)
events = self._resolve_schedule(shift_events)
events = schedule.final_events(user_tz, starting_date, days=30)
users = {}
for e in events:

View file

@ -83,7 +83,13 @@ logger.setLevel(logging.DEBUG)
# used for display schedule events on web
def list_of_oncall_shifts_from_ical(
schedule, date, user_timezone="UTC", with_empty_shifts=False, with_gaps=False, days=1
schedule,
date,
user_timezone="UTC",
with_empty_shifts=False,
with_gaps=False,
days=1,
filter_by=None,
):
"""
Parse the ical file and return list of events with users
@ -122,6 +128,9 @@ def list_of_oncall_shifts_from_ical(
else:
calendar_type = OnCallSchedule.OVERRIDES
if filter_by is not None and filter_by != calendar_type:
continue
tmp_result_datetime, tmp_result_date = get_shifts_dict(
calendar, calendar_type, schedule, datetime_start, datetime_end, date, with_empty_shifts
)

View file

@ -1,3 +1,5 @@
import datetime
import icalendar
from django.apps import apps
from django.conf import settings
@ -14,6 +16,7 @@ from apps.schedules.ical_utils import (
fetch_ical_file_or_get_error,
list_of_empty_shifts_in_schedule,
list_of_gaps_in_schedule,
list_of_oncall_shifts_from_ical,
list_users_to_notify_from_ical,
)
from apps.schedules.models import CustomOnCallShift
@ -222,6 +225,148 @@ class OnCallSchedule(PolymorphicModel):
self.cached_ical_file_overrides = None
self.save(update_fields=["cached_ical_file_overrides", "prev_ical_file_overrides"])
def filter_events(self, user_timezone, starting_date, days, with_empty=False, with_gap=False, filter_by=None):
"""Return filtered events from schedule."""
shifts = (
list_of_oncall_shifts_from_ical(
self, starting_date, user_timezone, with_empty, with_gap, days=days, filter_by=filter_by
)
or []
)
events = []
for shift in shifts:
all_day = type(shift["start"]) == datetime.date
is_gap = shift.get("is_gap", False)
shift_json = {
"all_day": all_day,
"start": shift["start"],
# fix confusing end date for all-day event
"end": shift["end"] - timezone.timedelta(days=1) if all_day else shift["end"],
"users": [
{
"display_name": user.username,
"pk": user.public_primary_key,
}
for user in shift["users"]
],
"missing_users": shift["missing_users"],
"priority_level": shift["priority"] if shift["priority"] != 0 else None,
"source": shift["source"],
"calendar_type": shift["calendar_type"],
"is_empty": len(shift["users"]) == 0 and not is_gap,
"is_gap": is_gap,
"is_override": shift["calendar_type"] == OnCallSchedule.TYPE_ICAL_OVERRIDES,
"shift": {
"pk": shift["shift_pk"],
},
}
events.append(shift_json)
return events
def final_events(self, user_tz, starting_date, days):
"""Return schedule final events, after resolving shifts and overrides."""
events = self.filter_events(user_tz, starting_date, days=days, with_empty=True, with_gap=True)
events = self._resolve_schedule(events)
return events
def _resolve_schedule(self, events):
"""Calculate final schedule shifts considering rotations and overrides."""
if not events:
return []
# sort schedule events by (type desc, priority desc, start timestamp asc)
events.sort(
key=lambda e: (
-e["calendar_type"] if e["calendar_type"] else 0, # overrides: 1, shifts: 0, gaps: None
-e["priority_level"] if e["priority_level"] else 0,
e["start"],
)
)
def _merge_intervals(evs):
"""Keep track of scheduled intervals."""
if not evs:
return []
intervals = [[e["start"], e["end"]] for e in evs]
result = [intervals[0]]
for interval in intervals[1:]:
previous_interval = result[-1]
if previous_interval[0] <= interval[0] <= previous_interval[1]:
previous_interval[1] = max(previous_interval[1], interval[1])
else:
result.append(interval)
return result
# iterate over events, reserving schedule slots based on their priority
# if the expected slot was already scheduled for a higher priority event,
# split the event, or fix start/end timestamps accordingly
# include overrides from start
resolved = [e for e in events if e["calendar_type"] == OnCallSchedule.TYPE_ICAL_OVERRIDES]
intervals = _merge_intervals(resolved)
pending = events[len(resolved) :]
if not pending:
return resolved
current_event_idx = 0 # current event to resolve
current_interval_idx = 0 # current scheduled interval being checked
current_priority = pending[0]["priority_level"] # current priority level being resolved
while current_event_idx < len(pending):
ev = pending[current_event_idx]
if ev["priority_level"] != current_priority:
# update scheduled intervals on priority change
# and start from the beginning for the new priority level
resolved.sort(key=lambda e: e["start"])
intervals = _merge_intervals(resolved)
current_interval_idx = 0
current_priority = ev["priority_level"]
if current_interval_idx >= len(intervals):
# event outside scheduled intervals, add to resolved
resolved.append(ev)
current_event_idx += 1
elif ev["start"] < intervals[current_interval_idx][0] and ev["end"] <= intervals[current_interval_idx][0]:
# event starts and ends outside an already scheduled interval, add to resolved
resolved.append(ev)
current_event_idx += 1
elif ev["start"] < intervals[current_interval_idx][0] and ev["end"] > intervals[current_interval_idx][0]:
# event starts outside interval but overlaps with an already scheduled interval
# 1. add a split event copy to schedule the time before the already scheduled interval
to_add = ev.copy()
to_add["end"] = intervals[current_interval_idx][0]
resolved.append(to_add)
# 2. check if there is still time to be scheduled after the current scheduled interval ends
if ev["end"] > intervals[current_interval_idx][1]:
# event ends after current interval, update event start timestamp to match the interval end
# and process the updated event as any other event
ev["start"] = intervals[current_interval_idx][1]
else:
# done, go to next event
current_event_idx += 1
elif ev["start"] >= intervals[current_interval_idx][0] and ev["end"] <= intervals[current_interval_idx][1]:
# event inside an already scheduled interval, ignore (go to next)
current_event_idx += 1
elif (
ev["start"] >= intervals[current_interval_idx][0]
and ev["start"] < intervals[current_interval_idx][1]
and ev["end"] > intervals[current_interval_idx][1]
):
# event starts inside a scheduled interval but ends out of it
# update the event start timestamp to match the interval end
ev["start"] = intervals[current_interval_idx][1]
# move to next interval and process the updated event as any other event
current_interval_idx += 1
elif ev["start"] >= intervals[current_interval_idx][1]:
# event starts after the current interval, move to next interval and go through it
current_interval_idx += 1
resolved.sort(key=lambda e: e["start"])
return resolved
class OnCallScheduleICal(OnCallSchedule):
# For the ical schedule both primary and overrides icals are imported via ical url

View file

@ -0,0 +1,322 @@
import pytest
from django.utils import timezone
from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleWeb
from common.constants.role import Role
@pytest.mark.django_db
def test_filter_events(make_organization, make_user_for_organization, make_schedule, make_on_call_shift):
organization = make_organization()
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleWeb,
name="test_web_schedule",
)
user = make_user_for_organization(organization)
viewer = make_user_for_organization(organization, role=Role.VIEWER)
now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
start_date = now - timezone.timedelta(days=7)
data = {
"start": start_date + timezone.timedelta(days=1, hours=10),
"rotation_start": start_date + timezone.timedelta(days=1, hours=10),
"duration": timezone.timedelta(hours=4),
"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([[user]])
# add empty shift
data = {
"start": start_date + timezone.timedelta(days=1, hours=20),
"rotation_start": start_date + timezone.timedelta(days=1, hours=20),
"duration": timezone.timedelta(hours=2),
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_WEEKLY,
"schedule": schedule,
}
empty_shift = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
)
empty_shift.add_rolling_users([[viewer]])
# override: 22-23
override_data = {
"start": start_date + timezone.timedelta(hours=22),
"rotation_start": start_date + timezone.timedelta(hours=22),
"duration": timezone.timedelta(hours=1),
"schedule": schedule,
}
override = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data
)
override.add_rolling_users([[user]])
# filter primary non-empty shifts only
events = schedule.filter_events("UTC", start_date, days=3, filter_by=OnCallSchedule.TYPE_ICAL_PRIMARY)
expected = [
{
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
"start": on_call_shift.start + timezone.timedelta(days=i),
"end": on_call_shift.start + timezone.timedelta(days=i) + on_call_shift.duration,
"all_day": False,
"is_override": False,
"is_empty": False,
"is_gap": False,
"priority_level": on_call_shift.priority_level,
"missing_users": [],
"users": [{"display_name": user.username, "pk": user.public_primary_key}],
"shift": {"pk": on_call_shift.public_primary_key},
"source": "api",
}
for i in range(2)
]
assert events == expected
# filter overrides only
events = schedule.filter_events("UTC", start_date, days=3, filter_by=OnCallSchedule.TYPE_ICAL_OVERRIDES)
expected_override = [
{
"calendar_type": OnCallSchedule.TYPE_ICAL_OVERRIDES,
"start": override.start,
"end": override.start + override.duration,
"all_day": False,
"is_override": True,
"is_empty": False,
"is_gap": False,
"priority_level": None,
"missing_users": [],
"users": [{"display_name": user.username, "pk": user.public_primary_key}],
"shift": {"pk": override.public_primary_key},
"source": "api",
}
]
assert events == expected_override
# no type filter
events = schedule.filter_events("UTC", start_date, days=3)
assert events == expected_override + expected
@pytest.mark.django_db
def test_filter_events_include_gaps(make_organization, make_user_for_organization, make_schedule, make_on_call_shift):
organization = make_organization()
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleWeb,
name="test_web_schedule",
)
user = make_user_for_organization(organization)
now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
start_date = now - timezone.timedelta(days=7)
data = {
"start": start_date + timezone.timedelta(hours=10),
"rotation_start": start_date + timezone.timedelta(days=1, hours=10),
"duration": timezone.timedelta(hours=8),
"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([[user]])
events = schedule.filter_events(
"UTC", start_date, days=1, filter_by=OnCallSchedule.TYPE_ICAL_PRIMARY, with_gap=True
)
expected = [
{
"calendar_type": None,
"start": start_date + timezone.timedelta(milliseconds=1),
"end": on_call_shift.start,
"all_day": False,
"is_override": False,
"is_empty": False,
"is_gap": True,
"priority_level": None,
"missing_users": [],
"users": [],
"shift": {"pk": None},
"source": None,
},
{
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
"start": on_call_shift.start,
"end": on_call_shift.start + on_call_shift.duration,
"all_day": False,
"is_override": False,
"is_empty": False,
"is_gap": False,
"priority_level": on_call_shift.priority_level,
"missing_users": [],
"users": [{"display_name": user.username, "pk": user.public_primary_key}],
"shift": {"pk": on_call_shift.public_primary_key},
"source": "api",
},
{
"calendar_type": None,
"start": on_call_shift.start + on_call_shift.duration,
"end": on_call_shift.start + timezone.timedelta(hours=13, minutes=59, seconds=59, milliseconds=1),
"all_day": False,
"is_override": False,
"is_empty": False,
"is_gap": True,
"priority_level": None,
"missing_users": [],
"users": [],
"shift": {"pk": None},
"source": None,
},
]
assert events == expected
@pytest.mark.django_db
def test_filter_events_include_empty(make_organization, make_user_for_organization, make_schedule, make_on_call_shift):
organization = make_organization()
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleWeb,
name="test_web_schedule",
)
user = make_user_for_organization(organization, role=Role.VIEWER)
now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
start_date = now - timezone.timedelta(days=7)
data = {
"start": start_date + timezone.timedelta(hours=10),
"rotation_start": start_date + timezone.timedelta(days=1, hours=10),
"duration": timezone.timedelta(hours=8),
"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([[user]])
events = schedule.filter_events(
"UTC", start_date, days=1, filter_by=OnCallSchedule.TYPE_ICAL_PRIMARY, with_empty=True
)
expected = [
{
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
"start": on_call_shift.start,
"end": on_call_shift.start + on_call_shift.duration,
"all_day": False,
"is_override": False,
"is_empty": True,
"is_gap": False,
"priority_level": on_call_shift.priority_level,
"missing_users": [user.username],
"users": [],
"shift": {"pk": on_call_shift.public_primary_key},
"source": "api",
}
]
assert events == expected
@pytest.mark.django_db
def test_final_schedule_events(make_organization, make_user_for_organization, make_on_call_shift, make_schedule):
organization = make_organization()
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleWeb,
name="test_web_schedule",
)
now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
start_date = now - timezone.timedelta(days=7)
user_a, user_b, user_c, user_d, user_e = (make_user_for_organization(organization, username=i) for i in "ABCDE")
shifts = (
# user, priority, start time (h), duration (hs)
(user_a, 1, 10, 5), # r1-1: 10-15 / A
(user_b, 1, 11, 2), # r1-2: 11-13 / B
(user_a, 1, 16, 3), # r1-3: 16-19 / A
(user_a, 1, 21, 1), # r1-4: 21-22 / A
(user_b, 1, 22, 2), # r1-5: 22-00 / B
(user_c, 2, 12, 2), # r2-1: 12-14 / C
(user_d, 2, 14, 1), # r2-2: 14-15 / D
(user_d, 2, 17, 1), # r2-3: 17-18 / D
(user_d, 2, 20, 3), # r2-4: 20-23 / D
)
for user, priority, start_h, duration in shifts:
data = {
"start": start_date + timezone.timedelta(hours=start_h),
"rotation_start": start_date + timezone.timedelta(hours=start_h),
"duration": timezone.timedelta(hours=duration),
"priority_level": priority,
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
"schedule": schedule,
}
on_call_shift = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_RECURRENT_EVENT, **data
)
on_call_shift.users.add(user)
# override: 22-23 / E
override_data = {
"start": start_date + timezone.timedelta(hours=22),
"rotation_start": start_date + timezone.timedelta(hours=22),
"duration": timezone.timedelta(hours=1),
"schedule": schedule,
}
override = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data
)
override.add_rolling_users([[user_e]])
returned_events = schedule.final_events("UTC", start_date, days=1)
expected = (
# start (h), duration (H), user, priority, is_gap, is_override
(0, 10, None, None, True, False), # 0-10 gap
(10, 2, "A", 1, False, False), # 10-12 A
(11, 1, "B", 1, False, False), # 11-12 B
(12, 2, "C", 2, False, False), # 12-14 C
(14, 1, "D", 2, False, False), # 14-15 D
(15, 1, None, None, True, False), # 15-16 gap
(16, 1, "A", 1, False, False), # 16-17 A
(17, 1, "D", 2, False, False), # 17-18 D
(18, 1, "A", 1, False, False), # 18-19 A
(19, 1, None, None, True, False), # 19-20 gap
(20, 2, "D", 2, False, False), # 20-22 D
(22, 1, "E", None, False, True), # 22-23 E (override)
(23, 1, "B", 1, False, False), # 23-00 B
)
expected_events = [
{
"calendar_type": 1 if is_override else None if is_gap else 0,
"end": start_date + timezone.timedelta(hours=start + duration),
"is_gap": is_gap,
"is_override": is_override,
"priority_level": priority,
"start": start_date + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0),
"user": user,
}
for start, duration, user, priority, is_gap, is_override in expected
]
returned_events = [
{
"calendar_type": e["calendar_type"],
"end": e["end"],
"is_gap": e["is_gap"],
"is_override": e["is_override"],
"priority_level": e["priority_level"],
"start": e["start"],
"user": e["users"][0]["display_name"] if e["users"] else None,
}
for e in returned_events
]
assert returned_events == expected_events

View file

@ -133,8 +133,6 @@ class AddToResolutionNoteStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari
).save()
else:
resolution_note.recreate()
alert_group.drop_cached_after_resolve_report_json()
alert_group.schedule_cache_for_web()
try:
self._slack_client.api_call(
"reactions.add",
@ -704,7 +702,7 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
# Show error message
resolution_note_data = json.loads(payload["actions"][0]["value"])
resolution_note_data["resolution_note_window_action"] = "edit_update_error"
return ResolutionNoteModalStep(slack_team_identity).process_scenario(
return ResolutionNoteModalStep(slack_team_identity, self.organization, self.user).process_scenario(
slack_user_identity,
slack_team_identity,
payload,

View file

@ -54,7 +54,7 @@ class SlackOAuth2V2(SlackOAuth2):
ACCESS_TOKEN_URL = "https://slack.com/api/oauth.v2.access"
AUTH_TOKEN_NAME = SLACK_AUTH_TOKEN_NAME
# Remove redirect state because we loose session during redirects
# Remove redirect state because we lose session during redirects
REDIRECT_STATE = False
STATE_PARAMETER = False