-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1378 from maykinmedia/task/2723-notification-back…
…ends [#2723] Support multiple backends for Notifications API
- Loading branch information
Showing
21 changed files
with
743 additions
and
10 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from django.contrib import admin, messages | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from requests.exceptions import RequestException | ||
|
||
from .models import NotificationsAPIConfig, Subscription | ||
|
||
|
||
@admin.register(NotificationsAPIConfig) | ||
class NotificationsConfigAdmin(admin.ModelAdmin): | ||
pass | ||
|
||
|
||
def register_webhook(modeladmin, request, queryset): | ||
for sub in queryset: | ||
if sub._subscription: | ||
continue | ||
|
||
try: | ||
sub.register() | ||
except RequestException as exc: | ||
messages.error( | ||
request, | ||
_( | ||
"Something went wrong while registering subscription " | ||
"for {callback}: {exception}" | ||
).format(callback=sub.callback_url, exception=exc), | ||
) | ||
|
||
|
||
register_webhook.short_description = _("Register the webhooks") # noqa | ||
|
||
|
||
@admin.register(Subscription) | ||
class SubscriptionAdmin(admin.ModelAdmin): | ||
list_display = ("callback_url", "channels", "_subscription") | ||
actions = [register_webhook] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from rest_framework import serializers | ||
|
||
from ..validators import UntilNowValidator | ||
|
||
|
||
class NotificatieSerializer(serializers.Serializer): | ||
kanaal = serializers.CharField( | ||
label=_("kanaal"), | ||
max_length=50, | ||
help_text=_( | ||
"De naam van het kanaal (`KANAAL.naam`) waar het bericht " | ||
"op moet worden gepubliceerd." | ||
), | ||
) | ||
hoofd_object = serializers.URLField( | ||
label=_("hoofd object"), | ||
help_text=_( | ||
"URL-referentie naar het hoofd object van de publicerende " | ||
"API die betrekking heeft op de `resource`." | ||
), | ||
) | ||
resource = serializers.CharField( | ||
label=_("resource"), | ||
max_length=100, | ||
help_text=_("De resourcenaam waar de notificatie over gaat."), | ||
) | ||
resource_url = serializers.URLField( | ||
label=_("resource URL"), | ||
help_text=_("URL-referentie naar de `resource` van de publicerende " "API."), | ||
) | ||
actie = serializers.CharField( | ||
label=_("actie"), | ||
max_length=100, | ||
help_text=_( | ||
"De actie die door de publicerende API is gedaan. De " | ||
"publicerende API specificeert de toegestane acties." | ||
), | ||
) | ||
aanmaakdatum = serializers.DateTimeField( | ||
label=_("aanmaakdatum"), | ||
validators=[UntilNowValidator()], | ||
help_text=_("Datum en tijd waarop de actie heeft plaatsgevonden."), | ||
) | ||
kenmerken = serializers.DictField( | ||
label=_("kenmerken"), | ||
required=False, | ||
child=serializers.CharField( | ||
label=_("kenmerk"), | ||
max_length=1000, | ||
help_text=_("Een waarde behorende bij de sleutel."), | ||
allow_blank=True, | ||
), | ||
help_text=_( | ||
"Mapping van kenmerken (sleutel/waarde) van de notificatie. De " | ||
"publicerende API specificeert de toegestane kenmerken." | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from django.apps import AppConfig | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
|
||
class NotificationsConfig(AppConfig): | ||
name = "notifications" | ||
verbose_name = _("Notifications API integration") | ||
default_auto_field = "django.db.models.BigAutoField" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
# Generated by Django 4.2.15 on 2024-09-04 10:18 | ||
|
||
import django.contrib.postgres.fields | ||
import django.db.models.deletion | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
("zgw_consumers", "0022_set_default_service_slug"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="NotificationsAPIConfig", | ||
fields=[ | ||
( | ||
"id", | ||
models.BigAutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
( | ||
"notification_delivery_max_retries", | ||
models.PositiveIntegerField( | ||
default=5, | ||
help_text="The maximum number of automatic retries. After this amount of retries, guaranteed delivery stops trying to deliver the message.", | ||
), | ||
), | ||
( | ||
"notification_delivery_retry_backoff", | ||
models.PositiveIntegerField( | ||
default=3, | ||
help_text="If specified, a factor applied to the exponential backoff. This allows you to tune how quickly automatic retries are performed.", | ||
), | ||
), | ||
( | ||
"notification_delivery_retry_backoff_max", | ||
models.PositiveIntegerField( | ||
default=48, | ||
help_text="An upper limit in seconds to the exponential backoff time.", | ||
), | ||
), | ||
( | ||
"notifications_api_service", | ||
models.ForeignKey( | ||
limit_choices_to={"api_type": "nrc"}, | ||
on_delete=django.db.models.deletion.PROTECT, | ||
related_name="notifications_api_configs", | ||
to="zgw_consumers.service", | ||
verbose_name="notifications api service", | ||
), | ||
), | ||
], | ||
options={ | ||
"verbose_name": "Notificatiescomponentenconfiguratie", | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name="Subscription", | ||
fields=[ | ||
( | ||
"id", | ||
models.BigAutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
( | ||
"callback_url", | ||
models.URLField( | ||
help_text="Where to send the notifications (webhook url)", | ||
verbose_name="callback url", | ||
), | ||
), | ||
( | ||
"client_id", | ||
models.CharField( | ||
help_text="Client ID to construct the auth token", | ||
max_length=50, | ||
verbose_name="client ID", | ||
), | ||
), | ||
( | ||
"secret", | ||
models.CharField( | ||
help_text="Secret to construct the auth token", | ||
max_length=50, | ||
verbose_name="client secret", | ||
), | ||
), | ||
( | ||
"channels", | ||
django.contrib.postgres.fields.ArrayField( | ||
base_field=models.CharField(max_length=100), | ||
help_text="Comma-separated list of channels to subscribe to", | ||
size=None, | ||
verbose_name="channels", | ||
), | ||
), | ||
( | ||
"_subscription", | ||
models.URLField( | ||
blank=True, | ||
editable=False, | ||
help_text="Subscription as it is known in the NC", | ||
verbose_name="NC subscription", | ||
), | ||
), | ||
( | ||
"notifications_api_config", | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.PROTECT, | ||
to="notifications.notificationsapiconfig", | ||
), | ||
), | ||
], | ||
options={ | ||
"verbose_name": "Webhook subscription", | ||
"verbose_name_plural": "Webhook subscriptions", | ||
}, | ||
), | ||
] |
126 changes: 126 additions & 0 deletions
126
src/notifications/migrations/0002_migrate_data_from_notifications_api_common.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import logging | ||
|
||
from django.db import migrations | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def migrate_from_notifications_api_common(apps, schema_editor): | ||
""" | ||
Migrate data for integration of Notificaties API from library to OIP | ||
""" | ||
try: | ||
LegacyNotificationsConfig = apps.get_model( | ||
"notifications_api_common", "NotificationsConfig" | ||
) | ||
LegacySubscription = apps.get_model("notifications_api_common", "Subscription") | ||
except LookupError as exc: | ||
exc.add_note( | ||
"notifications_api_common must be in INSTALLED_APPS in order to run the migrations" | ||
) | ||
raise | ||
|
||
NotificationsAPIConfig = apps.get_model("notifications", "NotificationsAPIConfig") | ||
Subscription = apps.get_model("notifications", "Subscription") | ||
|
||
# migrate notifications config | ||
try: | ||
legacy_config = LegacyNotificationsConfig.objects.get() | ||
except LegacyNotificationsConfig.DoesNotExist: | ||
return | ||
|
||
if not legacy_config.notifications_api_service: | ||
return | ||
|
||
config = NotificationsAPIConfig.objects.create( | ||
notifications_api_service=getattr( | ||
legacy_config, "notifications_api_service", None | ||
), | ||
notification_delivery_max_retries=legacy_config.notification_delivery_max_retries, | ||
notification_delivery_retry_backoff=legacy_config.notification_delivery_retry_backoff, | ||
notification_delivery_retry_backoff_max=legacy_config.notification_delivery_retry_backoff_max, | ||
) | ||
|
||
# migrate subscriptions | ||
legacy_subs = LegacySubscription.objects.all() | ||
for legacy_sub in legacy_subs: | ||
Subscription.objects.create( | ||
notifications_api_config=config, | ||
callback_url=legacy_sub.callback_url, | ||
client_id=legacy_sub.client_id, | ||
secret=legacy_sub.secret, | ||
channels=legacy_sub.channels, | ||
_subscription=legacy_sub._subscription, | ||
) | ||
|
||
|
||
def reverse_migrate_from_notifications_api_common(apps, schema_editor): | ||
""" | ||
Reverse-migrate data for integration of Notificaties API to library | ||
""" | ||
try: | ||
LegacyNotificationsConfig = apps.get_model( | ||
"notifications_api_common", "NotificationsConfig" | ||
) | ||
LegacySubscription = apps.get_model("notifications_api_common", "Subscription") | ||
except LookupError as exc: | ||
exc.add_note( | ||
"notifications_api_common must be in INSTALLED_APPS in order to run the migrations" | ||
) | ||
raise | ||
|
||
NotificationsAPIConfig = apps.get_model("notifications", "NotificationsAPIConfig") | ||
Subscription = apps.get_model("notifications", "Subscription") | ||
|
||
# reverse-migrate config(s) | ||
configs = NotificationsAPIConfig.objects.all() | ||
|
||
if configs.count() == 0: | ||
logger.info( | ||
"No configuration models for Notifications API found; " | ||
"skipping data migration for notifications_api_common" | ||
) | ||
return | ||
elif configs.count() > 1: | ||
raise ValueError( | ||
"Multiple configuration models for Notifications API found; " | ||
"reversing the migration for notifications_api_common requires that " | ||
"there be only one configuration model" | ||
) | ||
else: | ||
config = configs.get() | ||
LegacyNotificationsConfig.objects.create( | ||
notifications_api_service=getattr( | ||
config, "notifications_api_service", None | ||
), | ||
notification_delivery_max_retries=config.notification_delivery_max_retries, | ||
notification_delivery_retry_backoff=config.notification_delivery_retry_backoff, | ||
notification_delivery_retry_backoff_max=config.notification_delivery_retry_backoff_max, | ||
) | ||
|
||
# reverse-migrate subscriptions | ||
subs = Subscription.objects.all() | ||
for sub in subs: | ||
LegacySubscription.objects.create( | ||
callback_url=sub.callback_url, | ||
client_id=sub.client_id, | ||
secret=sub.secret, | ||
channels=sub.channels, | ||
_subscription=sub._subscription, | ||
) | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("notifications", "0001_initial"), | ||
( | ||
"notifications_api_common", | ||
"0008_merge_0006_auto_20221213_0214_0007_auto_20221206_0414", | ||
), | ||
] | ||
operations = [ | ||
migrations.RunPython( | ||
code=migrate_from_notifications_api_common, | ||
reverse_code=reverse_migrate_from_notifications_api_common, | ||
) | ||
] |
Empty file.
Oops, something went wrong.