Skip to content

Commit

Permalink
Merge pull request #523 from grafana/dev
Browse files Browse the repository at this point in the history
Merge dev to main
  • Loading branch information
matiasb authored Sep 12, 2022
2 parents f6809af + a606a6d commit dc77555
Show file tree
Hide file tree
Showing 122 changed files with 8,679 additions and 295 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## v1.0.36 (2022-09-12)
- Alpha web schedules frontend/backend updates
- Bug fixes

## v1.0.35 (2022-09-07)
- Bug fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def create_contact_points(self) -> None:
datasources, response_info = self.client.get_datasources()
if datasources is None:
logger.warning(
f"Failed to get datasource list for organization {self.alert_receive_channel.organization.org_title}, "
f"Failed to get datasource list for organization {self.alert_receive_channel.organization.stack_slug}, "
f"{response_info}"
)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def render(self):
incident_link = self.alert_group.web_link
return (
f"You are invited to check an incident #{self.alert_group.inside_organization_number} with title "
f'"{title}" in Grafana OnCall organization: "{self.alert_group.channel.organization.org_title}", '
f'"{title}" in Grafana OnCall organization: "{self.alert_group.channel.organization.stack_slug}", '
f"alert channel: {self.alert_group.channel.short_name}, "
f"alerts registered: {self.alert_group.alerts.count()}, "
f"{incident_link}\n"
Expand Down
22 changes: 6 additions & 16 deletions engine/apps/alerts/tasks/notify_ical_schedule_shift.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from apps.schedules.ical_events import ical_events
from apps.schedules.ical_utils import (
calculate_shift_diff,
event_start_end_all_day_with_respect_to_type,
get_icalendar_tz_or_utc,
get_usernames_from_ical_event,
ical_date_to_datetime,
is_icals_equal,
memoized_users_in_ical,
)
Expand All @@ -35,12 +35,7 @@ def get_current_shifts_from_ical(calendar, schedule, min_priority=0):
usernames, priority = get_usernames_from_ical_event(event)
users = memoized_users_in_ical(tuple(usernames), schedule.organization)
if len(users) > 0:
event_start, start_all_day = ical_date_to_datetime(
event["DTSTART"].dt,
calendar_tz,
start=True,
)
event_end, end_all_day = ical_date_to_datetime(event["DTEND"].dt, calendar_tz, start=False)
event_start, event_end, all_day_event = event_start_end_all_day_with_respect_to_type(event, calendar_tz)

if event["UID"] in shifts:
existing_event = shifts[event["UID"]]
Expand All @@ -50,7 +45,7 @@ def get_current_shifts_from_ical(calendar, schedule, min_priority=0):
"users": [u.pk for u in users],
"start": event_start,
"end": event_end,
"all_day": start_all_day,
"all_day": all_day_event,
"priority": priority + min_priority, # increase priority for overrides
"priority_increased_by": min_priority,
}
Expand All @@ -70,19 +65,14 @@ def get_next_shifts_from_ical(calendar, schedule, min_priority=0, days_to_lookup
usernames, priority = get_usernames_from_ical_event(event)
users = memoized_users_in_ical(tuple(usernames), schedule.organization)
if len(users) > 0:
event_start, start_all_day = ical_date_to_datetime(
event["DTSTART"].dt,
calendar_tz,
start=True,
)
event_end, end_all_day = ical_date_to_datetime(event["DTEND"].dt, calendar_tz, start=False)
event_start, event_end, all_day_event = event_start_end_all_day_with_respect_to_type(event, calendar_tz)

# next_shifts are not stored in db so we can use User objects directly
shifts[f"{event_start.timestamp()}_{event['UID']}"] = {
"users": users,
"start": event_start,
"end": event_end,
"all_day": start_all_day,
"all_day": all_day_event,
"priority": priority + min_priority, # increase priority for overrides
"priority_increased_by": min_priority,
}
Expand Down Expand Up @@ -265,7 +255,7 @@ def notify_ical_schedule_shift(schedule_pk):

for prev_ical_file, current_ical_file in prev_and_current_ical_files:
if prev_ical_file is not None and (
current_ical_file is None or not is_icals_equal(current_ical_file, prev_ical_file, schedule)
current_ical_file is None or not is_icals_equal(current_ical_file, prev_ical_file)
):
# If icals are not equal then compare current_events from them
is_prev_ical_diff = True
Expand Down
4 changes: 0 additions & 4 deletions engine/apps/api/serializers/on_call_shifts.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ def to_internal_value(self, data):
result = super().to_internal_value(data)
return result

def to_representation(self, instance):
result = super().to_representation(instance)
return result

def validate_by_day(self, by_day):
if by_day:
for day in by_day:
Expand Down
7 changes: 7 additions & 0 deletions engine/apps/api/serializers/schedule_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ScheduleBaseSerializer(EagerLoadingMixin, serializers.ModelSerializer):
user_group = UserGroupSerializer()
warnings = serializers.SerializerMethodField()
on_call_now = serializers.SerializerMethodField()
number_of_escalation_chains = serializers.SerializerMethodField()

class Meta:
fields = [
Expand All @@ -33,6 +34,7 @@ class Meta:
"notify_empty_oncall",
"mention_oncall_start",
"mention_oncall_next",
"number_of_escalation_chains",
]

SELECT_RELATED = ["organization"]
Expand Down Expand Up @@ -71,6 +73,11 @@ def get_on_call_now(self, obj):
else:
return []

def get_number_of_escalation_chains(self, obj):
# num_escalation_chains param added in queryset via annotate. Check ScheduleView.get_queryset
# return 0 for just created schedules
return getattr(obj, "num_escalation_chains", 0)

def validate(self, attrs):
if "slack_channel_id" in attrs:
slack_channel_id = attrs.pop("slack_channel_id", None)
Expand Down
29 changes: 25 additions & 4 deletions engine/apps/api/serializers/user.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
import time

import pytz
Expand Down Expand Up @@ -62,6 +63,7 @@ class Meta:
"permissions",
"notification_chain_verbal",
"cloud_connection_status",
"hide_phone_number",
]
read_only_fields = [
"email",
Expand Down Expand Up @@ -155,6 +157,24 @@ def get_cloud_connection_status(self, obj):
return status
return None

def to_representation(self, instance):
result = super().to_representation(instance)
if instance.id != self.context["request"].user.id:
if instance.hide_phone_number:
if result["verified_phone_number"]:
result["verified_phone_number"] = self._hide_phone_number(result["verified_phone_number"])
if result["unverified_phone_number"]:
result["unverified_phone_number"] = self._hide_phone_number(result["unverified_phone_number"])
return result

@staticmethod
def _hide_phone_number(number: str):
HIDE_SYMBOL = "*"
SHOW_LAST_SYMBOLS = 4
if len(number) <= 4:
SHOW_LAST_SYMBOLS = math.ceil(len(number) / 2)
return f"{HIDE_SYMBOL * (len(number) - SHOW_LAST_SYMBOLS)}{number[-SHOW_LAST_SYMBOLS:]}"


class UserHiddenFieldsSerializer(UserSerializer):
available_for_all_roles_fields = [
Expand All @@ -171,10 +191,11 @@ class UserHiddenFieldsSerializer(UserSerializer):

def to_representation(self, instance):
ret = super(UserSerializer, self).to_representation(instance)
for field in ret:
if field not in self.available_for_all_roles_fields:
ret[field] = "******"
ret["hidden_fields"] = True
if instance.id != self.context["request"].user.id:
for field in ret:
if field not in self.available_for_all_roles_fields:
ret[field] = "******"
ret["hidden_fields"] = True
return ret


Expand Down
140 changes: 122 additions & 18 deletions engine/apps/api/tests/test_oncall_shift.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,11 +462,17 @@ def test_update_old_on_call_shift_with_future_version(

response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token))

next_shift_start_date = timezone.datetime.combine(next_rotation_start_date.date(), start_date.time()).astimezone(
timezone.pytz.UTC
)

expected_payload = data_to_update | {
"id": new_on_call_shift.public_primary_key,
"type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT,
"schedule": schedule.public_primary_key,
"updated_shift": None,
"shift_start": next_shift_start_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
"shift_end": (next_shift_start_date + updated_duration).strftime("%Y-%m-%dT%H:%M:%SZ"),
}

assert response.status_code == status.HTTP_200_OK
Expand Down Expand Up @@ -1511,6 +1517,119 @@ def test_on_call_shift_preview_update(

now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
start_date = now - timezone.timedelta(days=7)
tomorrow = now + timezone.timedelta(days=1)

user = make_user_for_organization(organization)
other_user = make_user_for_organization(organization)

data = {
"start": start_date + timezone.timedelta(hours=8),
"rotation_start": start_date + timezone.timedelta(hours=8),
"duration": timezone.timedelta(hours=1),
"priority_level": 1,
"interval": 4,
"frequency": CustomOnCallShift.FREQUENCY_HOURLY,
"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]])

url = "{}?date={}&days={}".format(reverse("api-internal:oncall_shifts-preview"), tomorrow.strftime("%Y-%m-%d"), 1)
shift_start = (tomorrow + timezone.timedelta(hours=10)).strftime("%Y-%m-%dT%H:%M:%SZ")
shift_end = (tomorrow + timezone.timedelta(hours=18)).strftime("%Y-%m-%dT%H:%M:%SZ")
shift_data = {
"schedule": schedule.public_primary_key,
"shift_pk": on_call_shift.public_primary_key,
"type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT,
"rotation_start": shift_start,
"shift_start": shift_start,
"shift_end": shift_end,
"rolling_users": [[other_user.public_primary_key]],
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
}
response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK

# check rotation events
rotation_events = response.json()["rotation"]
assert len(rotation_events) == 4
# the final original rotation events are returned and the ID is kept
for shift in rotation_events[:3]:
assert shift["shift"]["pk"] == on_call_shift.public_primary_key
# previewing an update does not reuse shift PK if rotation already started
new_shift_pk = rotation_events[-1]["shift"]["pk"]
assert new_shift_pk != on_call_shift.public_primary_key
expected_shift_preview = {
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
"shift": {"pk": new_shift_pk},
"start": shift_start,
"end": shift_end,
"all_day": False,
"is_override": False,
"is_empty": False,
"is_gap": False,
"priority_level": 1,
"missing_users": [],
"users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}],
"source": "web",
}
assert rotation_events[-1] == expected_shift_preview

# check final schedule events
final_events = response.json()["final"]
expected = (
# start (h), duration (H), user, priority
(0, 1, user.username, 1), # 0-1 user
(4, 1, user.username, 1), # 4-5 user
(8, 1, user.username, 1), # 8-9 user
(10, 8, other_user.username, 1), # 10-18 other_user
)
expected_events = [
{
"end": (tomorrow + timezone.timedelta(hours=start + duration)).strftime("%Y-%m-%dT%H:%M:%SZ"),
"priority_level": priority,
"start": (tomorrow + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0)).strftime(
"%Y-%m-%dT%H:%M:%SZ"
),
"user": user,
}
for start, duration, user, priority in expected
]
returned_events = [
{
"end": e["end"],
"priority_level": e["priority_level"],
"start": e["start"],
"user": e["users"][0]["display_name"] if e["users"] else None,
}
for e in final_events
if not e["is_override"] and not e["is_gap"]
]
assert returned_events == expected_events


@pytest.mark.django_db
def test_on_call_shift_preview_update_not_started_reuse_pk(
make_organization_and_user_with_plugin_token,
make_user_for_organization,
make_user_auth_headers,
make_schedule,
make_on_call_shift,
):
organization, user, token = make_organization_and_user_with_plugin_token()
client = APIClient()

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)
request_date = start_date

user = make_user_for_organization(organization)
Expand All @@ -1533,7 +1652,7 @@ def test_on_call_shift_preview_update(
url = "{}?date={}&days={}".format(
reverse("api-internal:oncall_shifts-preview"), request_date.strftime("%Y-%m-%d"), 1
)
shift_start = (start_date + timezone.timedelta(hours=10)).strftime("%Y-%m-%dT%H:%M:%SZ")
shift_start = (start_date + timezone.timedelta(hours=6)).strftime("%Y-%m-%dT%H:%M:%SZ")
shift_end = (start_date + timezone.timedelta(hours=18)).strftime("%Y-%m-%dT%H:%M:%SZ")
shift_data = {
"schedule": schedule.public_primary_key,
Expand All @@ -1551,22 +1670,8 @@ def test_on_call_shift_preview_update(

# check rotation events
rotation_events = response.json()["rotation"]
# previewing an update reuses shift PK, so rotation keeps original event too
# previewing an update reuses shift PK when rotation is not started
expected_rotation_events = [
{
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
"shift": {"pk": on_call_shift.public_primary_key},
"start": on_call_shift.start.strftime("%Y-%m-%dT%H:%M:%SZ"),
"end": (on_call_shift.start + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"),
"all_day": False,
"is_override": False,
"is_empty": False,
"is_gap": False,
"priority_level": 1,
"missing_users": [],
"users": [{"display_name": user.username, "pk": user.public_primary_key}],
"source": "api",
},
{
"calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY,
"shift": {"pk": on_call_shift.public_primary_key},
Expand All @@ -1588,8 +1693,7 @@ def test_on_call_shift_preview_update(
final_events = response.json()["final"]
expected = (
# start (h), duration (H), user, priority
(8, 1, user.username, 1), # 8-9 user
(10, 8, other_user.username, 1), # 10-18 other_user
(6, 12, other_user.username, 1), # 6-18 other_user
)
expected_events = [
{
Expand Down
Loading

0 comments on commit dc77555

Please sign in to comment.