Skip to content

Commit

Permalink
Merge pull request #4460 from grafana/dev
Browse files Browse the repository at this point in the history
dev to main
  • Loading branch information
vadimkerr authored Jun 4, 2024
2 parents 80b9933 + f40634a commit b6b8bb2
Show file tree
Hide file tree
Showing 15 changed files with 689 additions and 92 deletions.
3 changes: 1 addition & 2 deletions docs/sources/set-up/open-source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,7 @@ Zvonok.com, complete the following steps:
to the variable `ZVONOK_AUDIO_ID` (optional step).
6. To make a call with a specific voice, you can set the `ZVONOK_SPEAKER_ID`.
By default, the ID used is `Salli` (optional step).
7. To change the voice message for phone verification, you can set the variable `ZVONOK_VERIFICATION_TEMPLATE`
with the following format (optional step): `Your verification code is $verification_code, have a nice day.`.
7. Create phone number verification campaign with type `tellcode` and assign its ID value to `ZVONOK_VERIFICATION_CAMPAIGN_ID`.
8. To process the call status, it is required to add a postback with the GET/POST method on the side of the zvonok.com
service with the following format (optional step):
`${ONCALL_BASE_URL}/zvonok/call_status_events?campaign_id={ct_campaign_id}&call_id={ct_call_id}&status={ct_status}&user_choice={ct_user_choice}`
Expand Down
6 changes: 6 additions & 0 deletions engine/apps/api/views/alert_group.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typing

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, Max, Q
from django_filters import rest_framework as filters
Expand Down Expand Up @@ -275,6 +276,11 @@ class AlertGroupView(
pagination_class = AlertGroupCursorPaginator

filter_backends = [SearchFilter, filters.DjangoFilterBackend]
search_fields = (
["=public_primary_key", "=inside_organization_number", "web_title_cache"]
if settings.FEATURE_ALERT_GROUP_SEARCH_ENABLED
else []
)
filterset_class = AlertGroupFilter

def get_serializer_class(self):
Expand Down
6 changes: 3 additions & 3 deletions engine/apps/base/models/live_setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class LiveSetting(models.Model):
"ZVONOK_POSTBACK_STATUS",
"ZVONOK_POSTBACK_USER_CHOICE",
"ZVONOK_POSTBACK_USER_CHOICE_ACK",
"ZVONOK_VERIFICATION_TEMPLATE",
"ZVONOK_VERIFICATION_CAMPAIGN_ID",
)

DESCRIPTIONS = {
Expand Down Expand Up @@ -170,7 +170,7 @@ class LiveSetting(models.Model):
"ZVONOK_POSTBACK_STATUS": "'Postback' status (ct_status) query parameter name to validate a postback request.",
"ZVONOK_POSTBACK_USER_CHOICE": "'Postback' user choice (ct_user_choice) query parameter name (optional).",
"ZVONOK_POSTBACK_USER_CHOICE_ACK": "'Postback' user choice (ct_user_choice) query parameter value for acknowledge alert group (optional).",
"ZVONOK_VERIFICATION_TEMPLATE": "The message template used for phone number verification (optional).",
"ZVONOK_VERIFICATION_CAMPAIGN_ID": "The phone number verification campaign ID. You can get it after verification campaign creation.",
}

SECRET_SETTING_NAMES = (
Expand Down Expand Up @@ -240,7 +240,7 @@ def populate_settings_if_needed(cls):

@classmethod
def validate_settings(cls):
settings_to_validate = cls.objects.all()
settings_to_validate = cls.objects.filter(name__in=cls.AVAILABLE_NAMES)
for setting in settings_to_validate:
setting.error = LiveSettingValidator(live_setting=setting).get_error()
setting.save(update_fields=["error"])
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/slack/scenarios/paging.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ def _create_user_option_groups(
"text": f"{user.name or user.username}",
"emoji": True,
},
"value": make_value({"id": user.pk}, organization),
"value": json.dumps({"id": user.pk}),
}
for user in users
]
Expand Down
40 changes: 17 additions & 23 deletions engine/apps/zvonok/phone_provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
from random import randint
from string import Template
from typing import Optional

import requests
Expand All @@ -12,6 +11,7 @@
from apps.zvonok.models.phone_call import ZvonokCallStatuses, ZvonokPhoneCall

ZVONOK_CALL_URL = "https://zvonok.com/manager/cabapi_external/api/v1/phones/call/"
ZVONOK_VERIFICATION_CALL_URL = "https://zvonok.com/manager/cabapi_external/api/v1/phones/tellcode/"

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -96,6 +96,15 @@ def _call_create(self, number: str, text: str, speaker: Optional[str] = None):

return requests.post(ZVONOK_CALL_URL, params=params)

def _verification_call_create(self, number: str, code: int):
params = {
"public_key": live_settings.ZVONOK_API_KEY,
"campaign_id": live_settings.ZVONOK_VERIFICATION_CAMPAIGN_ID,
"phone": number,
"pincode": code,
}
return requests.post(ZVONOK_VERIFICATION_CALL_URL, params=params)

def _get_graceful_msg(self, body, number):
if body:
status = body.get("status")
Expand All @@ -105,34 +114,19 @@ def _get_graceful_msg(self, body, number):
return f"Failed make call to {number}"

def make_verification_call(self, number: str):
body = None
code = self._generate_verification_code()
cache.set(self._cache_key(number), code, timeout=10 * 60)
codewspaces = " ".join(code)

body = None
speaker = live_settings.ZVONOK_SPEAKER_ID

if live_settings.ZVONOK_VERIFICATION_TEMPLATE:
message = Template(live_settings.ZVONOK_VERIFICATION_TEMPLATE).safe_substitute(
verification_code=codewspaces
if not live_settings.ZVONOK_VERIFICATION_CAMPAIGN_ID:
raise FailedToStartVerification(
graceful_msg="Failed make verification call, verification campaign id not set."
)
else:
message = f"Your verification code is {codewspaces}"

try:
response = self._call_create(
number,
message,
speaker,
)
response.raise_for_status()
response = self._verification_call_create(number, code)
body = response.json()
if not body:
logger.error("ZvonokPhoneProvider.make_verification_call: failed, empty body")
raise FailedToMakeCall(graceful_msg=f"Failed make verification call to {number}, empty body")

call_id = body.get("call_id")
if not call_id:
raise FailedToStartVerification(graceful_msg=self._get_graceful_msg(body, number))
response.raise_for_status()
except requests.exceptions.HTTPError as http_err:
logger.error(f"ZvonokPhoneProvider.make_verification_call: failed {http_err}")
raise FailedToStartVerification(graceful_msg=self._get_graceful_msg(body, number))
Expand Down
50 changes: 21 additions & 29 deletions engine/apps/zvonok/tests/test_zvonok_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest
from django.test import override_settings

from apps.phone_notifications.exceptions import FailedToStartVerification
from apps.zvonok.phone_provider import ZvonokPhoneProvider


Expand All @@ -12,46 +13,37 @@ def provider():


@pytest.mark.django_db
def test_make_verification_call_with_template_set(provider):
verification_code = "123456"
def test_make_verification_call(provider):
verification_code = "123456789"
number = "1234567890"
speaker_id = "Salli"
template_value = 'Your code is <prosody rate="x-slow">$verification_code</prosody>'
excepted_message = 'Your code is <prosody rate="x-slow">1 2 3 4 5 6</prosody>'

with override_settings(ZVONOK_VERIFICATION_TEMPLATE=template_value, ZVONOK_SPEAKER_ID=speaker_id):
campaign_id = "123456"
with override_settings(ZVONOK_VERIFICATION_CAMPAIGN_ID=campaign_id):
with patch("django.core.cache.cache.set"):
provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"}))
provider._verification_call_create = MagicMock(return_value=MagicMock(json=lambda: {"status": "ok"}))
provider._generate_verification_code = MagicMock(return_value=verification_code)
provider.make_verification_call(number)
provider._call_create.assert_called_once_with(number, excepted_message, speaker_id)
provider._verification_call_create.assert_called_once_with(number, verification_code)


@pytest.mark.django_db
def test_make_verification_call_with_invalid_template_set(provider):
verification_code = "123456"
def test_make_verification_call_without_campaign_id(provider):
number = "1234567890"
speaker_id = "Salli"
template_value = "Your code is"
excepted_message = "Your code is"

with override_settings(ZVONOK_VERIFICATION_TEMPLATE=template_value, ZVONOK_SPEAKER_ID=speaker_id):
with patch("django.core.cache.cache.set"):
provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"}))
provider._generate_verification_code = MagicMock(return_value=verification_code)
with patch("django.core.cache.cache.set"):
with pytest.raises(FailedToStartVerification):
provider.make_verification_call(number)
provider._call_create.assert_called_once_with(number, excepted_message, speaker_id)


@pytest.mark.django_db
def test_make_verification_call_without_template_set(provider):
verification_code = "123456"
def test_make_verification_call_with_error(provider):
number = "1234567890"
speaker_id = "Salli"
excepted_message = "Your verification code is 1 2 3 4 5 6"
with override_settings(ZVONOK_SPEAKER_ID=speaker_id):
campaign_id = "123456"

with override_settings(ZVONOK_VERIFICATION_CAMPAIGN_ID=campaign_id):
with patch("django.core.cache.cache.set"):
provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"}))
provider._generate_verification_code = MagicMock(return_value=verification_code)
provider.make_verification_call(number)
provider._call_create.assert_called_once_with(number, excepted_message, speaker_id)
with pytest.raises(FailedToStartVerification):
provider._verification_call_create = MagicMock(
return_value=MagicMock(
json={"status": "error", "data": "Form isn't valid: * campaign_id\n * Invalid campaign type"}
)
)
provider.make_verification_call(number)
3 changes: 2 additions & 1 deletion engine/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
FEATURE_LABELS_ENABLED_FOR_ALL = getenv_boolean("FEATURE_LABELS_ENABLED_FOR_ALL", default=False)
# Enable labels feature for organizations from the list. Use OnCall organization ID, for this flag
FEATURE_LABELS_ENABLED_PER_ORG = getenv_list("FEATURE_LABELS_ENABLED_PER_ORG", default=list())
FEATURE_ALERT_GROUP_SEARCH_ENABLED = getenv_boolean("FEATURE_ALERT_GROUP_SEARCH_ENABLED", default=False)

TWILIO_API_KEY_SID = os.environ.get("TWILIO_API_KEY_SID")
TWILIO_API_KEY_SECRET = os.environ.get("TWILIO_API_KEY_SECRET")
Expand Down Expand Up @@ -912,7 +913,7 @@ class BrokerTypes:
ZVONOK_POSTBACK_STATUS = os.getenv("ZVONOK_POSTBACK_STATUS", "status")
ZVONOK_POSTBACK_USER_CHOICE = os.getenv("ZVONOK_POSTBACK_USER_CHOICE", None)
ZVONOK_POSTBACK_USER_CHOICE_ACK = os.getenv("ZVONOK_POSTBACK_USER_CHOICE_ACK", None)
ZVONOK_VERIFICATION_TEMPLATE = os.getenv("ZVONOK_VERIFICATION_TEMPLATE", None)
ZVONOK_VERIFICATION_CAMPAIGN_ID = os.getenv("ZVONOK_VERIFICATION_CAMPAIGN_ID", None)

DETACHED_INTEGRATIONS_SERVER = getenv_boolean("DETACHED_INTEGRATIONS_SERVER", default=False)

Expand Down
37 changes: 20 additions & 17 deletions grafana-plugin/src/components/GTable/GTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useCallback, useMemo, ChangeEvent, ReactElement } from 'react';

import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Pagination, Checkbox, Icon, useStyles2 } from '@grafana/ui';
import Table from 'rc-table';
import { TableProps } from 'rc-table/lib/Table';
Expand Down Expand Up @@ -136,7 +135,7 @@ export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props:
}, [rowSelection, columnsProp, data]);

return (
<div className={styles.root} data-testid="test__gTable">
<div className={cx(styles.root, { [styles.fixed]: props.tableLayout === 'fixed' })} data-testid="test__gTable">
<Table<RT>
expandable={expandable}
rowKey={rowKey}
Expand All @@ -156,20 +155,24 @@ export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props:
);
};

const getGTableStyles = (_theme: GrafanaTheme2) => {
return {
root: css`
table {
width: 100%;
}
`,
const getGTableStyles = () => ({
root: css`
table {
width: 100%;
}
`,

fixed: css`
table {
table-layout: fixed;
}
`,

pagination: css`
margin-top: 20px;
`,
pagination: css`
margin-top: 20px;
`,

checkbox: css`
display: inline-flex;
`,
};
};
checkbox: css`
display: inline-flex;
`,
});
17 changes: 11 additions & 6 deletions grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import React, { FC, useEffect } from 'react';

import { cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Badge, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui';
import { Badge, BadgeColor, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui';
import { observer } from 'mobx-react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { CSSTransition, TransitionGroup } from 'react-transition-group';

import { Avatar } from 'components/Avatar/Avatar';
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
import { Text } from 'components/Text/Text';
import { Rotation } from 'containers/Rotation/Rotation';
import { TimelineMarks } from 'containers/TimelineMarks/TimelineMarks';
Expand Down Expand Up @@ -102,14 +103,18 @@ const _SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotC
<div className={styles.header}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<Text type="secondary">
On-call schedule <Avatar src={storeUser.avatar} size="small" /> {storeUser.username}
</Text>
<RenderConditionally
shouldRender={Boolean(storeUser)}
render={() => (
<Text type="secondary">
On-call schedule <Avatar src={storeUser.avatar} size="small" /> {storeUser.username}
</Text>
)}
/>
{isOncall ? (
<Badge text="On-call now" color="green" />
) : (
/* @ts-ignore */
<Badge text="Not on-call now" color="gray" />
<Badge text="Not on-call now" color={'gray' as BadgeColor} />
)}
</HorizontalGroup>
<HorizontalGroup>
Expand Down
2 changes: 1 addition & 1 deletion grafana-plugin/src/models/alertgroup/alertgroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class AlertGroupStore {
params: {
query: {
...incidentFilters,
search,
search: incidentFilters?.search || search,
perpage: this.alertsSearchResult?.page_size,
cursor: this.incidentsCursor,
is_root: true,
Expand Down
28 changes: 20 additions & 8 deletions grafana-plugin/src/pages/schedule/Schedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -598,23 +598,35 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
history.replace(`${PLUGIN_ROOT}/schedules`);
};

handleShowShiftSwapForm = (id: ShiftSwap['id'] | 'new') => {
handleShowShiftSwapForm = (id: ShiftSwap['id'] | 'new', swap?: { swap_start: string; swap_end: string }) => {
const { filters } = this.state;
const {
store,
store: {
userStore: { currentUserPk },
timezoneStore: { currentDateInSelectedTimezone },
},
match: {
params: { id: scheduleId },
},
} = this.props;

const {
userStore: { currentUserPk },
timezoneStore: { currentDateInSelectedTimezone },
} = store;

const layers = getLayersFromStore(store, scheduleId, store.timezoneStore.calendarStartDate);
if (swap) {
if (!filters.users.includes(currentUserPk)) {
this.setState({ filters: { ...filters, users: [...this.state.filters.users, currentUserPk] } });
this.highlightMyShiftsWasToggled = true;
}

const { filters } = this.state;
return this.setState({
shiftSwapIdToShowForm: id,
shiftSwapParamsToShowForm: {
swap_start: swap.swap_start,
swap_end: swap.swap_end,
},
});
}

const layers = getLayersFromStore(store, scheduleId, store.timezoneStore.calendarStartDate);
const closestEvent = findClosestUserEvent(dayjs(), currentUserPk, layers);
const swapStart = closestEvent
? dayjs(closestEvent.start)
Expand Down
1 change: 1 addition & 0 deletions grafana-plugin/src/pages/schedules/Schedules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
total: results ? Math.ceil((count || 0) / page_size) : 0,
onChange: this.handlePageChange,
}}
tableLayout="fixed"
rowKey="id"
expandable={{
expandedRowKeys: expandedRowKeys,
Expand Down
2 changes: 1 addition & 1 deletion grafana-plugin/src/styles/utils.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const getUtilStyles = (theme: GrafanaTheme2) => {

thinLineBreak: css`
width: 100%;
border-top: 1px solid ${theme.colors.secondary.main};
border-top: 1px solid ${theme.colors.secondary.contrastText};
margin-top: 8px;
opacity: 15%;
`,
Expand Down
Loading

0 comments on commit b6b8bb2

Please sign in to comment.