Skip to content

Commit

Permalink
Merge pull request #14 from maykinmedia/feature/kanaal-kenmerken-nest…
Browse files Browse the repository at this point in the history
…ed-fields

✨ [open-zaak/open-notificaties#156] Several changes to Kanalen
  • Loading branch information
stevenbal authored Dec 17, 2024
2 parents 8a6907f + 48b9571 commit 3300aed
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 58 deletions.
45 changes: 38 additions & 7 deletions notifications_api_common/kanalen.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,35 @@
"""

from collections import defaultdict
from typing import Dict, Tuple
from typing import Dict, Literal, Tuple

from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db.models import Model
from django.db.models.base import ModelBase
from django.db.models import Field, Model

from rest_framework.request import Request

KANAAL_REGISTRY = set()


class Kanaal:
def __init__(self, label: str, main_resource: ModelBase, kenmerken: Tuple = None):
def __init__(
self,
label: str,
main_resource: Model,
kenmerken: Tuple | None = None,
extra_kwargs: dict[str, dict[Literal["help_text"], str]] | None = None,
):
self.label = label
self.main_resource = main_resource
self.extra_kwargs = extra_kwargs or {}

self.usage = defaultdict(list) # filled in by metaclass of notifications

# check that we're refering to existing fields
self.kenmerken = kenmerken or ()
for kenmerk in self.kenmerken:
try:
self.main_resource._meta.get_field(kenmerk)
self.get_field(self.main_resource, kenmerk)
except FieldDoesNotExist as exc:
raise ImproperlyConfigured(
f"Kenmerk '{kenmerk}' does not exist on the model {main_resource}"
Expand All @@ -39,7 +47,28 @@ def __repr__(self):
self.main_resource,
)

def get_kenmerken(self, obj: Model, data: Dict = None) -> Dict:
@staticmethod
def get_field(model: Model, field_name: str) -> Field:
"""
Function to retrieve a field from a Model
"""
return model._meta.get_field(field_name)

def get_help_text(self, field: Field, kenmerk: str) -> str:
"""
Retrieve the help_text for a kenmerk, pulled from the model field by default,
but can be overridden by setting extra_kwargs on `Kanaal.__init__`
"""
if help_text := self.extra_kwargs.get(kenmerk, {}).get("help_text"):
return help_text
return field.help_text

def get_kenmerken(
self,
obj: Model,
data: dict | None = None,
request: Request | None = None, # noqa
) -> Dict:
data = data or {}
return {
kenmerk: data.get(kenmerk, getattr(obj, kenmerk))
Expand All @@ -55,7 +84,9 @@ def description(self):
kenmerken = [
kenmerk_template.format(
kenmerk=kenmerk,
help_text=self.main_resource._meta.get_field(kenmerk).help_text,
help_text=self.get_help_text(
self.get_field(self.main_resource, kenmerk), kenmerk
),
)
for kenmerk in self.kenmerken
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.conf import settings
from django.core.management import BaseCommand
from django.template.loader import render_to_string

from notifications_api_common.kanalen import KANAAL_REGISTRY


class Command(BaseCommand):
"""
Generate a markdown file documenting the notification channels of the component
"""

def add_arguments(self, parser):
super().add_arguments(parser)

parser.add_argument(
"--output-file",
dest="output_file",
default=None,
help="Name of the output file",
)

def handle(self, output_file, *args, **options):
kanalen = sorted(KANAAL_REGISTRY, key=lambda s: s.label)

template = "notifications_api_common/notificaties.md"
markdown = render_to_string(
template,
context={
"kanalen": kanalen,
"project_name": settings.PROJECT_NAME,
"site_title": settings.SITE_TITLE,
},
)

with open(output_file, "w") as f:
f.write(markdown)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Notificaties
## Berichtkenmerken voor {{ project_name }} API

Kanalen worden typisch per component gedefinieerd. Producers versturen berichten op bepaalde kanalen,
consumers ontvangen deze. Consumers abonneren zich via een notificatiecomponent (zoals {{ 'https://notificaties-api.vng.cloud/api/v1/schema/'|urlize }}) op berichten.

Hieronder staan de kanalen beschreven die door deze component gebruikt worden, met de kenmerken bij elk bericht.

De architectuur van de notificaties staat beschreven op {{ 'https://github.com/VNG-Realisatie/notificaties-api'|urlize }}.

{% for kanaal in kanalen %}
### {{ kanaal.label }}

**Kanaal**
`{{ kanaal.label }}`

{{ kanaal.description|default:""|urlize }}

**Resources en acties**

{% for resource, actions in kanaal.get_usage %}
* <code>{{ resource }}</code>: {{ actions|join:", " }}
{% endfor %}
{% endfor %}
4 changes: 3 additions & 1 deletion notifications_api_common/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ def construct_message(self, data: dict, instance: models.Model = None) -> dict:
"actie": self.action,
"aanmaakdatum": timezone.now(),
# each channel knows which kenmerken it has, so delegate this
"kenmerken": kanaal.get_kenmerken(main_object, main_object_data),
"kenmerken": kanaal.get_kenmerken(
main_object, main_object_data, request=getattr(self, "request", None)
),
}

# let the serializer & render machinery shape the data the way it
Expand Down
8 changes: 6 additions & 2 deletions testapp/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ class PersonViewSet(NotificationViewSetMixin, viewsets.ModelViewSet):
serializer_class = PersonSerializer
notifications_kanaal = Kanaal(
"personen",
main_resource=Person,
kenmerken=("address_street",),
main_resource=Person(),
kenmerken=("name", "address_street"),
extra_kwargs={"address_street": {"help_text": "custom help text"}},
)

def get_notification_main_object_url(self, data: dict, kanaal: Kanaal):
return data["url"]


# URL routing
router = routers.DefaultRouter()
Expand Down
3 changes: 3 additions & 0 deletions testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

SECRET_KEY = "so-secret-i-cant-believe-you-are-looking-at-this"

PROJECT_NAME = "Notifications API Common"
SITE_TITLE = "Notifications API Common"

DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from notifications_api_common.models import NotificationsConfig
from testapp import urls # noqa

NOTIFICATIONS_API_ROOT = "http://some-api-root/api/v1/"


def dummy_get_response(request):
raise NotImplementedError()
Expand Down
51 changes: 51 additions & 0 deletions tests/test_generate_notificaties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from unittest.mock import mock_open, patch

from django.test.testcases import call_command

import pytest

EXPECTED_OUTPUT = """## Notificaties
## Berichtkenmerken voor Notifications API Common API
Kanalen worden typisch per component gedefinieerd. Producers versturen berichten op bepaalde kanalen,
consumers ontvangen deze. Consumers abonneren zich via een notificatiecomponent (zoals <a href="https://notificaties-api.vng.cloud/api/v1/schema/" rel="nofollow">https://notificaties-api.vng.cloud/api/v1/schema/</a>) op berichten.
Hieronder staan de kanalen beschreven die door deze component gebruikt worden, met de kenmerken bij elk bericht.
De architectuur van de notificaties staat beschreven op <a href="https://github.com/VNG-Realisatie/notificaties-api" rel="nofollow">https://github.com/VNG-Realisatie/notificaties-api</a>.
### personen
**Kanaal**
`personen`
**Main resource**
`person`
**Kenmerken**
* `name`: The name of the person
* `address_street`: custom help text
**Resources en acties**
* <code>person</code>: create, update, destroy
"""


@pytest.mark.django_db
@patch(
"notifications_api_common.management.commands.generate_notificaties.open",
new_callable=mock_open,
)
def test_generate_notificaties(mock_file):
call_command("generate_notificaties", output_file=["foobar"])

mock_file().write.assert_called_once_with(EXPECTED_OUTPUT)
17 changes: 17 additions & 0 deletions tests/test_kanaal_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,20 @@ def test_generate_docs():
"""

assert result == expected


def test_kanaal_get_help_text():
kanaal = Kanaal(label="dummy", main_resource=Person, kenmerken=("name",))
field = kanaal.get_field(Person, "name")

assert kanaal.get_help_text(field, "name") == "The name of the person"

kanaal = Kanaal(
label="dummy",
main_resource=Person,
kenmerken=("name",),
extra_kwargs={"name": {"help_text": "help text 2"}},
)
field = kanaal.get_field(Person, "name")

assert kanaal.get_help_text(field, "name") == "help text 2"
77 changes: 50 additions & 27 deletions tests/test_register_kanalen.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@

from notifications_api_common.kanalen import KANAAL_REGISTRY, Kanaal

kanalen = set(
(
Kanaal(label="foobar", main_resource=Mock()),
Kanaal(label="boofar", main_resource=Mock()),

@pytest.fixture
def override_kanalen():

kanalen = set(
(
Kanaal(label="foobar", main_resource=Mock()),
Kanaal(label="boofar", main_resource=Mock()),
)
)
)

KANAAL_REGISTRY.clear()
KANAAL_REGISTRY.update(kanalen)
KANAAL_REGISTRY.clear()
KANAAL_REGISTRY.update(kanalen)


@pytest.mark.django_db
def test_register_kanalen_success(notifications_config, requests_mock):
def test_register_kanalen_success(
notifications_config, requests_mock, override_kanalen
):
kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"
params = urlencode(dict(naam="foobar"))

Expand Down Expand Up @@ -58,27 +64,38 @@ def test_register_kanalen_success(notifications_config, requests_mock):


@pytest.mark.django_db
def test_register_kanalen_from_registry_success(notifications_config, requests_mock):
def test_register_kanalen_from_registry_success(
notifications_config, requests_mock, override_kanalen
):
kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"

url_mapping = {
kanaal.label: f"{kanaal_url}?{urlencode(dict(naam=kanaal.label))}"
for kanaal in kanalen
"foobar": f"{kanaal_url}?{urlencode(dict(naam='foobar'))}",
"boofar": f"{kanaal_url}?{urlencode(dict(naam='boofar'))}",
}

for kanaal in kanalen:
requests_mock.get(url_mapping[kanaal.label], json=[])

requests_mock.post(
kanaal_url,
json={
"url": "http://example.com",
"naam": kanaal.label,
"documentatieLink": "http://example.com",
"filters": ["string"],
},
status_code=201,
)
requests_mock.get(url_mapping["foobar"], json=[])
requests_mock.post(
kanaal_url,
json={
"url": "http://example.com",
"naam": "foobar",
"documentatieLink": "http://example.com",
"filters": ["string"],
},
status_code=201,
)
requests_mock.get(url_mapping["boofar"], json=[])
requests_mock.post(
kanaal_url,
json={
"url": "http://example.com",
"naam": "boofar",
"documentatieLink": "http://example.com",
"filters": ["string"],
},
status_code=201,
)

reverse_patch = (
"notifications_api_common.management.commands.register_kanalen.reverse"
Expand Down Expand Up @@ -106,7 +123,9 @@ def test_register_kanalen_from_registry_success(notifications_config, requests_m


@pytest.mark.django_db
def test_register_kanalen_existing_kanalen(notifications_config, requests_mock):
def test_register_kanalen_existing_kanalen(
notifications_config, requests_mock, override_kanalen
):
"""
Test that already registered kanalen does not cause issues
"""
Expand Down Expand Up @@ -135,7 +154,9 @@ def test_register_kanalen_existing_kanalen(notifications_config, requests_mock):


@pytest.mark.django_db
def test_register_kanalen_unknown_url(notifications_config, requests_mock):
def test_register_kanalen_unknown_url(
notifications_config, requests_mock, override_kanalen
):
kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"
params = urlencode(dict(naam="foobar"))

Expand All @@ -157,7 +178,9 @@ def test_register_kanalen_unknown_url(notifications_config, requests_mock):


@pytest.mark.django_db
def test_register_kanalen_incorrect_post(notifications_config, requests_mock):
def test_register_kanalen_incorrect_post(
notifications_config, requests_mock, override_kanalen
):
kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"
params = urlencode(dict(naam="foobar"))

Expand Down
Loading

0 comments on commit 3300aed

Please sign in to comment.