Source code for argus.notificationprofile.media.base
from __future__ import annotations
import logging
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from django.db import transaction
from argus.constants import API_STABLE_VERSION
from argus.notificationprofile.models import DestinationConfig
from argus.notificationprofile.utils import are_notifications_enabled
if TYPE_CHECKING:
from collections.abc import Iterable
from types import NoneType
from django.contrib.auth import get_user_model
from django.db.models.query import QuerySet
from argus.incident.models import Event
from ..serializers import RequestDestinationConfigSerializer
User = get_user_model()
__all__ = ["NotificationMedium"]
LOG = logging.getLogger(__name__)
[docs]
class NotificationMedium(ABC):
class NotDeletableError(Exception):
"""
Custom exception class that is raised when a destination cannot be
deleted
"""
def __init__(self, version: str = API_STABLE_VERSION):
self.version = version
[docs]
@classmethod
@abstractmethod
def validate(cls, instance: RequestDestinationConfigSerializer, dict: dict, user: User) -> dict:
"""
Validates the settings of destination and returns a dict with
validated and cleaned data
"""
pass
[docs]
@classmethod
@abstractmethod
def has_duplicate(cls, queryset: QuerySet, settings: dict) -> bool:
"""
Returns True if a destination with the given settings already exists
in the given queryset
"""
pass
[docs]
@staticmethod
@abstractmethod
def get_label(destination: DestinationConfig) -> str:
"""
Returns a descriptive label for this destination.
"""
pass
@classmethod
def get_relevant_address(cls, destination: DestinationConfig) -> Any:
"""
Returns the "address" to send the message to
The type of the address depends on the medium, it must be something
``cls.send()`` understands.
"""
raise NotImplementedError
@classmethod
def get_relevant_destinations(cls, destinations: Iterable[DestinationConfig]) -> set[DestinationConfig]:
"Return only destinations of the correct type"
return set(dest for dest in destinations if dest.media_id == cls.MEDIA_SLUG)
# XXX: deprecated! Use decorator when on Python 3.13
@classmethod
def get_relevant_addresses(cls, destinations: Iterable[DestinationConfig]) -> set[Any]:
"""
Returns a set of addresses the message should be sent to
Deprecated: Use ``cls.get_relevant_destinations`` with
``cls.get_relevant_address`` in a loop instead, in order to make it
possible to log every tried destination.
"""
addresses = [
cls.get_relevant_address(destination) for destination in cls.get_relevant_destinations(destinations)
]
return set(addresses)
[docs]
@classmethod
def send(cls, event: Event, destinations: Iterable[DestinationConfig], **kwargs) -> bool:
"""
Sends message about a given event to the given destinations
Loops over the destinations from ``cls.get_relevant_destinations`` and
converts each destination to a medium-specific "address" via
``cls.get_relevant_address``.
Returns a boolean:
* True: everything ok
* False: at least one destination failed
"""
if not are_notifications_enabled():
LOG.info("notifications: turned off sitewide, not sending")
return False
[docs]
@classmethod
def raise_if_not_deletable(cls, destination: DestinationConfig) -> NoneType:
"""
Raises a NotDeletableError if the given destination cannot be deleted
Potential reasons:
* it is marked as "managed", which means it is usable but read-only for end-users
* it is in use by at least one notification profile
"""
if destination.managed:
raise cls.NotDeletableError("Cannot delete this destination since it was defined by an outside source.")
connected_profiles = destination.notification_profiles.all()
if connected_profiles:
profiles = ", ".join([str(profile) for profile in connected_profiles])
raise cls.NotDeletableError(
f"Cannot delete this destination since it is in use in the notification profile(s): {profiles}."
)
[docs]
@staticmethod
@transaction.atomic()
def update(destination: DestinationConfig, validated_data: dict) -> DestinationConfig | NoneType:
"""Updates a destination
If the destination is marked as managed, a copy of the original will be
made before changing the destination.
"""
if destination.managed:
# clone the mananged destination so that it doesn't need to be resynced
managed_destination = DestinationConfig(
user=destination.user,
media_id=destination.media_id,
# don't create a label in order to avoid duplicate label
settings=destination.settings,
managed=True,
)
managed_destination.save()
# update destination with known id instead of returning a new one
destination.label = validated_data.get("label", destination.label)
destination.settings = validated_data.get("settings", destination.settings)
destination.managed = False
destination.save()
return destination