Skip to content

Commit

Permalink
Merge pull request #1378 from maykinmedia/task/2723-notification-back…
Browse files Browse the repository at this point in the history
…ends

[#2723] Support multiple backends for Notifications API
  • Loading branch information
alextreme authored Sep 16, 2024
2 parents 2c574e4 + 912095b commit 7f55115
Show file tree
Hide file tree
Showing 21 changed files with 743 additions and 10 deletions.
Empty file added src/notifications/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions src/notifications/admin.py
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.
59 changes: 59 additions & 0 deletions src/notifications/api/serializers.py
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."
),
)
8 changes: 8 additions & 0 deletions src/notifications/apps.py
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"
131 changes: 131 additions & 0 deletions src/notifications/migrations/0001_initial.py
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",
},
),
]
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.
Loading

0 comments on commit 7f55115

Please sign in to comment.