Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move common functionality to destination base class #936

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.d/+cleanup-destination-plugins.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Moved a lot of common infrastructure from our NotificationMedium subclasses to
the parent. This should make it easier to create more media of high quality.
Also, it should make it easier to use the plugins for validating the
settings-file elsewhere than just the API.

This might break 3rd party notification plugins.
144 changes: 101 additions & 43 deletions docs/integrations/notifications/writing-notification-plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,49 @@ implement the following:
Class constants
---------------

You need to set the constants `MEDIA_SLUG`, `MEDIA_NAME` and
`MEDIA_JSON_SCHEMA`.

The media name is the name of the service you want to send notifications by.
This is used only for display purposes so you might want to keep it short and
sweet. So for example `Email`, `SMS` or `MS Teams`.

The media slug is the slugified version of that, so the name simplified to only
contain lowercase letters, numbers, underscores and hyphens. Always have it
start with a letter, a-z. For example `email`, `sms` or `msteams`.

The media `json schema <https://json-schema.org/>`_ is a representation of how
a destination that will be used by this notification plugin should look like.
Such a destination should include all necessary information that is needed to
send notifications with your notification plugin. In case of SMS that is a
phone number or for MS Teams a webhook.
You must set the constants ``MEDIA_SLUG`, `MEDIA_NAME`` and
``MEDIA_JSON_SCHEMA``. If your plugin only takes or needs a single
configuration flag you should also set ``MEDIA_SETTINGS_KEY``.

MEDIA_NAME
The media name is the name of the service you want to send notifications by.
This is used only for display purposes so you might want to keep it short
and sweet. So for example ``"Email"``, ``"SMS"`` or ``"MS Teams"``.

MEDIA_SLUG
The media slug is the slugified version of that, so the name simplified to
only contain lowercase letters, numbers, underscores and hyphens. Always
have it start with a letter, a-z. For example ``"email"``, ``"sms"`` or
``"msteams"``.

MEDIA_JSON_SCHEMA
The media `json schema <https://json-schema.org/>`_ is a representation of
how a destination that will be used by this notification plugin should look
like, so that it is possible to autogenerate a form with javascript. It will
be accessible via the API. Such a destination should include all necessary
information that is needed to send notifications with your notification
plugin. In case of SMS that is a phone number or for MS Teams a webhook.

MEDIA_SETTINGS_KEY
The media settings key is the name of the most important key in the settings
JSON field. It is used to cut down on the amount of code you need to write
if there is only one piece of config you need to send the notification.
Among other things, it is used to check for duplicate entries, so in a way
it acts as the primary key for your plugin. For that reason, it must be
required in the json schema. For example for an email plugin this would be
"email_address".

Form
The ``forms.Form`` used to validate the settings-field.

Class methods for sending notifications
---------------------------------------

.. autoclass:: argus.notificationprofile.media.base.NotificationMedium
:members: send

This MUST be overridden.

The ``send`` method is the method that does the actual sending of the
notification. It gets the Argus event and a list of destinations as input and
returns a boolean indicating if the sending was successful.
Expand All @@ -62,35 +82,73 @@ The rest is very dependent on the notification medium and, if used, the Python
library. The given event can be used to extract relevant information that
should be included in the message that will be sent to each destination.

Class methods for destinations
------------------------------
Helper class methods
--------------------

.. autoclass:: argus.notificationprofile.media.base.NotificationMedium
:members: get_label, has_duplicate, raise_if_not_deletable, update, validate
:noindex:

Your implementation of ``get_label`` should show a reasonable representation
for a destination of that type that makes it easy to identify. For SMS that
would simply be the phone number.

The method ``has_duplicate`` will receive a QuerySet of destinations and a dict
of settings for a possible destination and should return True if a destination
with such settings exists in the given QuerySet.

``raise_if_not_deletable`` should check if a given destination can be deleted.
This is used in case some destinations are synced from an outside source and
should not be able to be deleted by a user. If that is the case a
``NotDeletableError`` should be raised. If not simply return None.

The method ``update`` only has to be implemented if the regular update method
of Django isn't sufficient. This can be the case if additional settings need to
be updated.

Finally the function ``validate`` makes sure that a destination with the given
settings can be updated or created. The function ``has_duplicate`` can be used
here to ensure that not two destinations with the same settings will be
created. Additionally the settings themselves should also be validated. For
example for SMS the given phone number will be checked. Django forms can be
helpful for validation. A ``ValidationError`` should be raised if the given
settings are invalid and the validated and cleaned data should be returned if
not.
With a little luck you might not need to override any of these.

clean
This method will do any additional cleaning beyond what is defined by the
defined ``Form``. Expects a valid form instance and optional
DestinationConfig instance, and returns the updated valid form instance. If
you have fields that shouldn't be set by a user, or values that need extra
conversion, you can do that in this method. Use the passed in instance if
you need to fall back to defaults. This method should not be used to
validate anything and thus should never raise a validation Exception.

get_label
Your implementation of ``get_label`` should show a reasonable representation
for a destination of that type that makes it easy to identify. For SMS that
would simply be the phone number. By default it shows the label stored in
the destination. If no label has been set, it uses MEDIA_SETTINGS_KEY to
look up the most important piece of information in the settings and uses
that directly. The included plugins need not override ``get_label`` for this
reason. If the label would be very long, for instance if the needed setting
is a very long url (40+ characters), you ought to write your own
``get_label``.

get_relevant_destination_settings
Used by ``send``. You should only need to override this if the key in
MEDIA_SETTINGS_KEY is insuffcient to look up the actual configuration of the
destinations of the type set by MEDIA_SLUG.

has_duplicate
The method ``has_duplicate`` will receive a QuerySet of destinations and
a dict of settings for a possible destination and should return True if
a destination with such settings exists in the given QuerySet. By default it
will use MEDIA_SETTINGS_KEY to lookup the most important piece of
information in the settings.

raise_if_not_deletable
``raise_if_not_deletable`` should check if a given destination can be
deleted. This is necessary in case the destination is in use by a profile,
or some destinations are synced from an outside source or otherwise
auto-generated, and should not be able to be deleted by a user. If that is
the case a ``NotDeletableError`` should be raised. If not simply return
None.

update
The method ``update`` only has to be implemented if the regular update
method is insufficient. This can be the case if there is more than one
key-value pair in settings that need to be updated.

validate
The function ``validate`` makes sure that a destination with the given
settings can be updated or created. It uses the ``validate_settings`` method
to validate the settings-field, a form (CommonDestinationConfigForm) to
validate the media and label-fields, and an optional DestinationConfig
instance for the sake of the ``clean``-method. The validated form is
returned if ok, otherwise a ``ValidationError`` should be raised. It is
unlikely that you will ever need to override this method.

validate_settings
This method validates the actual contents of the settings-field using the
``Form`` that is defined and an optional DestinationConfig instance for the
sake of the ``clean``-method. The function ``has_duplicate`` can be used
here to ensure that no two destinations with the same settings will be
created. A ``ValidationError`` should be raised if the given settings are
invalid, and the validated and cleaned data should be returned if not.
12 changes: 11 additions & 1 deletion src/argus/notificationprofile/media/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

LOG = logging.getLogger(__name__)


__all__ = [
"api_safely_get_medium_object",
"send_notification",
Expand All @@ -42,6 +41,17 @@
MEDIA_CLASSES_DICT = {media_class.MEDIA_SLUG: media_class for media_class in _media_classes}


class DestinationPluginError(Exception):
pass


def get_medium_object(media_slug: str):
try:
return MEDIA_CLASSES_DICT[str(media_slug)]
except KeyError as e:
raise DestinationPluginError(f'Medium "{media_slug}" is not installed.') from e


def api_safely_get_medium_object(media_slug):
try:
obj = MEDIA_CLASSES_DICT[media_slug]
Expand Down
Loading
Loading