commit
f2be57169b
13 changed files with 107 additions and 39 deletions
|
|
@ -511,17 +511,8 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
|
||||
@property
|
||||
def telegram_permalink(self) -> typing.Optional[str]:
|
||||
"""
|
||||
This property will attempt to access an attribute, `prefetched_telegram_messages`, representing a list of
|
||||
prefetched telegram messages. If this attribute does not exist, it falls back to performing a query.
|
||||
|
||||
See `apps.public_api.serializers.incidents.IncidentSerializer.PREFETCH_RELATED` as an example.
|
||||
"""
|
||||
from apps.telegram.models.message import TelegramMessage
|
||||
|
||||
if hasattr(self, "prefetched_telegram_messages"):
|
||||
return self.prefetched_telegram_messages[0].link if self.prefetched_telegram_messages else None
|
||||
|
||||
main_telegram_message = self.telegram_messages.filter(
|
||||
chat_id__startswith="-", message_type=TelegramMessage.ALERT_GROUP_MESSAGE
|
||||
).first()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import typing
|
|||
from django.conf import settings
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from apps.google import constants, utils
|
||||
from apps.google.types import GoogleCalendarEvent as GoogleCalendarEventType
|
||||
|
|
@ -23,6 +24,11 @@ class GoogleCalendarEvent:
|
|||
self.end_time_utc = self._end_time.astimezone(datetime.timezone.utc)
|
||||
|
||||
|
||||
class GoogleCalendarHTTPError(Exception):
|
||||
def __init__(self, http_error) -> None:
|
||||
self.error = http_error
|
||||
|
||||
|
||||
class GoogleCalendarAPIClient:
|
||||
MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH = 250
|
||||
"""
|
||||
|
|
@ -68,17 +74,22 @@ class GoogleCalendarAPIClient:
|
|||
now + datetime.timedelta(days=constants.DAYS_IN_FUTURE_TO_CONSIDER_OUT_OF_OFFICE_EVENTS)
|
||||
)
|
||||
|
||||
events_result = (
|
||||
self.service.events()
|
||||
.list(
|
||||
calendarId=self.CALENDAR_ID,
|
||||
timeMin=time_min,
|
||||
timeMax=time_max,
|
||||
maxResults=self.MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH,
|
||||
singleEvents=True,
|
||||
orderBy="startTime",
|
||||
eventTypes="outOfOffice",
|
||||
try:
|
||||
events_result = (
|
||||
self.service.events()
|
||||
.list(
|
||||
calendarId=self.CALENDAR_ID,
|
||||
timeMin=time_min,
|
||||
timeMax=time_max,
|
||||
maxResults=self.MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH,
|
||||
singleEvents=True,
|
||||
orderBy="startTime",
|
||||
eventTypes="outOfOffice",
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
except HttpError as e:
|
||||
logger.error(f"GoogleCalendarAPIClient - Error fetching out of office events: {e}")
|
||||
raise GoogleCalendarHTTPError(e)
|
||||
|
||||
return [GoogleCalendarEvent(event) for event in events_result.get("items", [])]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import logging
|
|||
from celery.utils.log import get_task_logger
|
||||
|
||||
from apps.google import constants
|
||||
from apps.google.client import GoogleCalendarAPIClient
|
||||
from apps.google.client import GoogleCalendarAPIClient, GoogleCalendarHTTPError
|
||||
from apps.google.models import GoogleOAuth2User
|
||||
from apps.schedules.models import OnCallSchedule, ShiftSwapRequest
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
|
@ -31,7 +31,13 @@ def sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk: int) -> N
|
|||
if oncall_schedules_to_consider_for_shift_swaps:
|
||||
users_schedules = users_schedules.filter(public_primary_key__in=oncall_schedules_to_consider_for_shift_swaps)
|
||||
|
||||
for out_of_office_event in google_api_client.fetch_out_of_office_events():
|
||||
try:
|
||||
out_of_office_events = google_api_client.fetch_out_of_office_events()
|
||||
except GoogleCalendarHTTPError:
|
||||
logger.info(f"Failed to fetch out of office events for user {user_id}")
|
||||
return
|
||||
|
||||
for out_of_office_event in out_of_office_events:
|
||||
raw_event = out_of_office_event.raw_event
|
||||
|
||||
event_title = raw_event["summary"]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
from django.utils import timezone
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from apps.google import constants, tasks
|
||||
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb, ShiftSwapRequest
|
||||
|
|
@ -140,6 +141,28 @@ def test_setup(
|
|||
return _test_setup
|
||||
|
||||
|
||||
class MockResponse:
|
||||
def __init__(self, reason=None, status=200) -> None:
|
||||
self.reason = reason or ""
|
||||
self.status = status
|
||||
|
||||
|
||||
@patch("apps.google.client.build")
|
||||
@pytest.mark.django_db
|
||||
def test_sync_out_of_office_calendar_events_for_user_httperror(mock_google_api_client_build, test_setup):
|
||||
mock_response = MockResponse(reason="forbidden", status=403)
|
||||
mock_google_api_client_build.return_value.events.return_value.list.return_value.execute.side_effect = HttpError(
|
||||
resp=mock_response, content=b"error"
|
||||
)
|
||||
|
||||
google_oauth2_user, schedule = test_setup([])
|
||||
user = google_oauth2_user.user
|
||||
|
||||
tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user.pk)
|
||||
|
||||
assert ShiftSwapRequest.objects.filter(beneficiary=user, schedule=schedule).count() == 0
|
||||
|
||||
|
||||
@patch("apps.google.client.build")
|
||||
@pytest.mark.django_db
|
||||
def test_sync_out_of_office_calendar_events_for_user_no_ooo_events(mock_google_api_client_build, test_setup):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from django.db.models import Prefetch
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.alerts.models import AlertGroup
|
||||
from apps.telegram.models.message import TelegramMessage
|
||||
from common.api_helpers.custom_fields import UserIdField
|
||||
from common.api_helpers.mixins import EagerLoadingMixin
|
||||
|
||||
|
|
@ -19,14 +17,6 @@ class IncidentSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
resolved_by = UserIdField(read_only=True, source="resolved_by_user")
|
||||
|
||||
SELECT_RELATED = ["channel", "channel_filter", "slack_message", "channel__organization"]
|
||||
PREFETCH_RELATED = [
|
||||
"alerts",
|
||||
Prefetch(
|
||||
"telegram_messages",
|
||||
TelegramMessage.objects.filter(chat_id__startswith="-", message_type=TelegramMessage.ALERT_GROUP_MESSAGE),
|
||||
to_attr="prefetched_telegram_messages",
|
||||
),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
model = AlertGroup
|
||||
|
|
@ -50,7 +40,7 @@ class IncidentSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
return obj.web_title_cache
|
||||
|
||||
def get_alerts_count(self, obj):
|
||||
return len(obj.alerts.all())
|
||||
return obj.alerts.count()
|
||||
|
||||
def get_state(self, obj):
|
||||
return obj.state
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ class SlackAPIMessageNotFoundError(SlackAPIError):
|
|||
errors = ("message_not_found",)
|
||||
|
||||
|
||||
class SlackAPICantUpdateMessageError(SlackAPIError):
|
||||
errors = ("cant_update_message",)
|
||||
|
||||
|
||||
class SlackAPIUserNotFoundError(SlackAPIError):
|
||||
errors = ("user_not_found",)
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class SlackMessage(models.Model):
|
|||
|
||||
@property
|
||||
def deep_link(self) -> str:
|
||||
return f"slack://channel?team={self.slack_team_identity.slack_id}&id={self.channel_id}&message={self.slack_id}"
|
||||
return f"https://slack.com/app_redirect?channel={self.channel_id}&team={self.slack_team_identity.slack_id}&message={self.slack_id}"
|
||||
|
||||
def send_slack_notification(self, user, alert_group, notification_policy):
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from apps.alerts.models import Alert, AlertGroup, AlertGroupLogRecord, AlertRece
|
|||
from apps.api.permissions import RBACPermission
|
||||
from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME
|
||||
from apps.slack.errors import (
|
||||
SlackAPICantUpdateMessageError,
|
||||
SlackAPIChannelArchivedError,
|
||||
SlackAPIChannelInactiveError,
|
||||
SlackAPIChannelNotFoundError,
|
||||
|
|
@ -947,6 +948,7 @@ class UpdateLogReportMessageStep(scenario_step.ScenarioStep):
|
|||
SlackAPIChannelArchivedError,
|
||||
SlackAPIChannelInactiveError,
|
||||
SlackAPIInvalidAuthError,
|
||||
SlackAPICantUpdateMessageError,
|
||||
):
|
||||
pass
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.alerts.models import AlertGroup
|
||||
from apps.slack.errors import SlackAPIRestrictedActionError
|
||||
from apps.slack.errors import SlackAPICantUpdateMessageError, SlackAPIRestrictedActionError
|
||||
from apps.slack.models import SlackMessage
|
||||
from apps.slack.scenarios.distribute_alerts import AlertShootingStep
|
||||
from apps.slack.scenarios.distribute_alerts import AlertShootingStep, UpdateLogReportMessageStep
|
||||
from apps.slack.scenarios.scenario_step import ScenarioStep
|
||||
from apps.slack.tests.conftest import build_slack_response
|
||||
|
||||
|
|
@ -64,3 +65,37 @@ def test_alert_shooting_no_channel_filter(
|
|||
|
||||
mock_post_alert_group_to_slack.assert_called_once()
|
||||
assert mock_post_alert_group_to_slack.call_args[1]["channel_id"] == "DEFAULT_CHANNEL_ID"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_log_report_cant_update(
|
||||
make_slack_team_identity,
|
||||
make_organization,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
organization = make_organization(
|
||||
slack_team_identity=slack_team_identity, general_log_channel_id="DEFAULT_CHANNEL_ID"
|
||||
)
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
|
||||
alert_group = make_alert_group(alert_receive_channel, channel_filter=None)
|
||||
# alert = make_alert(alert_group, raw_request_data={})
|
||||
log_message = make_slack_message(
|
||||
alert_group=alert_group,
|
||||
channel_id="RANDOM_CHANNEL_ID",
|
||||
slack_id="RANDOM_MESSAGE_ID",
|
||||
last_updated=timezone.now() - timezone.timedelta(minutes=5),
|
||||
)
|
||||
alert_group.slack_log_message = log_message
|
||||
|
||||
step = UpdateLogReportMessageStep(slack_team_identity, organization)
|
||||
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
|
||||
mock_slack_api_call.side_effect = SlackAPICantUpdateMessageError(
|
||||
response=build_slack_response({"error": "cant_update_message"})
|
||||
)
|
||||
# not raising error, will not retry
|
||||
step.update_log_message(alert_group)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from slack_sdk.web import SlackResponse
|
|||
from apps.slack.client import SlackClient, server_error_retry_handler
|
||||
from apps.slack.errors import (
|
||||
SlackAPICannotDMBotError,
|
||||
SlackAPICantUpdateMessageError,
|
||||
SlackAPIChannelArchivedError,
|
||||
SlackAPIChannelInactiveError,
|
||||
SlackAPIChannelNotFoundError,
|
||||
|
|
@ -116,6 +117,7 @@ def test_slack_client_generic_error(mock_request, monkeypatch, make_organization
|
|||
[
|
||||
("account_inactive", SlackAPITokenError),
|
||||
("cannot_dm_bot", SlackAPICannotDMBotError),
|
||||
("cant_update_message", SlackAPICantUpdateMessageError),
|
||||
("channel_not_found", SlackAPIChannelNotFoundError),
|
||||
("fatal_error", SlackAPIServerError),
|
||||
("fetch_members_failed", SlackAPIFetchMembersFailedError),
|
||||
|
|
|
|||
|
|
@ -56,5 +56,8 @@ def test_slack_message_deep_link(
|
|||
slack_channel = make_slack_channel(slack_team_identity)
|
||||
slack_message = make_slack_message(alert_group=alert_group, channel_id=slack_channel.slack_id)
|
||||
|
||||
expected = f"slack://channel?team={slack_team_identity.slack_id}&id={slack_channel.slack_id}&message={slack_message.slack_id}"
|
||||
expected = (
|
||||
f"https://slack.com/app_redirect?channel={slack_channel.slack_id}"
|
||||
f"&team={slack_team_identity.slack_id}&message={slack_message.slack_id}"
|
||||
)
|
||||
assert slack_message.deep_link == expected
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ module = [
|
|||
"fcm_django.*",
|
||||
"firebase_admin.*",
|
||||
"googleapiclient.discovery.*",
|
||||
"googleapiclient.errors.*",
|
||||
"google.oauth2.credentials.*",
|
||||
"httpretty.*",
|
||||
"humanize.*",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const getApiPathByPage = (page: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const convertFiltersToBackendFormat = (filters: FiltersValues, filterOptions: FilterOption[]) => {
|
||||
export const convertFiltersToBackendFormat = (filters: FiltersValues = {}, filterOptions: FilterOption[] = []) => {
|
||||
const newFilters = { ...filters };
|
||||
filterOptions.forEach((filterOption) => {
|
||||
if (filterOption.type === 'daterange' && newFilters[filterOption.name]) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue