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

[#2723] Support multiple backends for Notifications API #1378

Merged
merged 4 commits into from
Sep 16, 2024
Merged
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
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),
pi-sigma marked this conversation as resolved.
Show resolved Hide resolved
)


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
Loading