diff --git a/src/open_inwoner/accounts/choices.py b/src/open_inwoner/accounts/choices.py
index f88fc04597..f8e4e462c0 100644
--- a/src/open_inwoner/accounts/choices.py
+++ b/src/open_inwoner/accounts/choices.py
@@ -68,3 +68,8 @@ class EmptyStatusChoices(models.TextChoices):
class TypeChoices(models.TextChoices):
incidental = "incidental", _("Incidentieel")
recurring = "recurring", _("Terugkerend")
+
+
+class NotificationChannelChoice(models.TextChoices):
+ digital_and_post = "digital_and_post", _("Digitaal en per brief")
+ digital_only = "digital_only", _("Alleen digitaal")
diff --git a/src/open_inwoner/accounts/forms.py b/src/open_inwoner/accounts/forms.py
index 8a619d9d6f..f11d221857 100644
--- a/src/open_inwoner/accounts/forms.py
+++ b/src/open_inwoner/accounts/forms.py
@@ -192,6 +192,7 @@ class Meta:
"cases_notifications",
"messages_notifications",
"plans_notifications",
+ "case_notification_channel",
)
def __init__(self, user, *args, **kwargs):
@@ -204,6 +205,9 @@ def __init__(self, user, *args, **kwargs):
self.fields["last_name"].required = True
# notifications
+ if not config.enable_notification_channel_choice:
+ del self.fields["case_notification_channel"]
+
if (
not user.login_type == LoginTypeChoices.digid
or not config.notifications_cases_enabled
@@ -322,6 +326,7 @@ class Meta:
"cases_notifications",
"messages_notifications",
"plans_notifications",
+ "case_notification_channel",
)
def __init__(self, user, *args, **kwargs) -> None:
@@ -329,6 +334,9 @@ def __init__(self, user, *args, **kwargs) -> None:
config = SiteConfiguration.get_solo()
+ if not config.enable_notification_channel_choice:
+ del self.fields["case_notification_channel"]
+
if (
not config.notifications_cases_enabled
or not case_page_is_published()
diff --git a/src/open_inwoner/accounts/migrations/0076_user_case_notification_channel.py b/src/open_inwoner/accounts/migrations/0076_user_case_notification_channel.py
new file mode 100644
index 0000000000..f5eb20b6a8
--- /dev/null
+++ b/src/open_inwoner/accounts/migrations/0076_user_case_notification_channel.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.11 on 2024-08-07 10:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("accounts", "0075_user_verified_email"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="user",
+ name="case_notification_channel",
+ field=models.CharField(
+ choices=[
+ ("digital_and_post", "Digitaal en per brief"),
+ ("digital_only", "Alleen digitaal"),
+ ],
+ default="digital_and_post",
+ verbose_name="Case notifications channel",
+ ),
+ ),
+ ]
diff --git a/src/open_inwoner/accounts/models.py b/src/open_inwoner/accounts/models.py
index 394dfdf8d6..a7f64fb0d8 100644
--- a/src/open_inwoner/accounts/models.py
+++ b/src/open_inwoner/accounts/models.py
@@ -31,7 +31,13 @@
)
from ..plans.models import PlanContact
-from .choices import ContactTypeChoices, LoginTypeChoices, StatusChoices, TypeChoices
+from .choices import (
+ ContactTypeChoices,
+ LoginTypeChoices,
+ NotificationChannelChoice,
+ StatusChoices,
+ TypeChoices,
+)
from .managers import ActionQueryset, DigidManager, UserManager, eHerkenningManager
from .query import InviteQuerySet, MessageQuerySet
@@ -207,6 +213,11 @@ def has_verified_email(self):
"Indicates if the user wants to receive notifications for updates concerning cases."
),
)
+ case_notification_channel = models.CharField(
+ verbose_name=_("Case notifications channel"),
+ choices=NotificationChannelChoice.choices,
+ default=NotificationChannelChoice.digital_and_post,
+ )
messages_notifications = models.BooleanField(
verbose_name=_("Messages notifications"),
default=True,
diff --git a/src/open_inwoner/accounts/templates/accounts/registration_necessary.html b/src/open_inwoner/accounts/templates/accounts/registration_necessary.html
index a074ca8575..49a56e114a 100644
--- a/src/open_inwoner/accounts/templates/accounts/registration_necessary.html
+++ b/src/open_inwoner/accounts/templates/accounts/registration_necessary.html
@@ -69,6 +69,20 @@
{% trans "Notification preferences" %}
{% trans "E-mailnotificaties wanneer er actie nodig is voor een zaak (kan niet uitgeschakeld worden)" %}
+ {# Choice of zaken notification channel #}
+ {% if form.case_notification_channel %}
+ {% trans "How do you want to receive notifications about cases?" %}
+
+ {% with form.case_notification_channel as field %}
+ {% for choice in field.field.choices %}
+
+ {% choice_radio_stacked choice=choice name=field.name data=field.value index=forloop.counter initial=field.form.initial icon_class=choice.1|get_icon_class %}
+
+ {% endfor %}
+ {% endwith %}
+
+ {% endif %}
+
{% form_actions primary_icon='east' primary_text="Voltooi registratie" fullwidth=True %}
{% endrender_column %}
diff --git a/src/open_inwoner/accounts/tests/test_auth.py b/src/open_inwoner/accounts/tests/test_auth.py
index 193e2677b9..44bd12d595 100644
--- a/src/open_inwoner/accounts/tests/test_auth.py
+++ b/src/open_inwoner/accounts/tests/test_auth.py
@@ -16,11 +16,14 @@
OpenIDConnectDigiDConfig,
OpenIDConnectEHerkenningConfig,
)
+from open_inwoner.accounts.choices import NotificationChannelChoice
from open_inwoner.configurations.models import SiteConfiguration
from open_inwoner.haalcentraal.tests.mixins import HaalCentraalMixin
from open_inwoner.kvk.branches import get_kvk_branch_number
from open_inwoner.kvk.tests.factories import CertificateFactory
+from open_inwoner.openklant.tests.data import MockAPIReadPatchData
from open_inwoner.openzaak.models import OpenZaakConfig
+from open_inwoner.utils.tests.helpers import AssertTimelineLogMixin
from ...cms.collaborate.cms_apps import CollaborateApphook
from ...cms.profile.cms_apps import ProfileApphook
@@ -42,7 +45,9 @@
@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls")
-class DigiDRegistrationTest(AssertRedirectsMixin, HaalCentraalMixin, WebTest):
+class DigiDRegistrationTest(
+ AssertRedirectsMixin, AssertTimelineLogMixin, HaalCentraalMixin, WebTest
+):
"""Tests concerning the registration of DigiD users"""
csrf_checks = False
@@ -235,12 +240,24 @@ def test_user_can_modify_only_email_when_digid_and_brp(self, m):
self.assertEqual(user.first_name, "Merel")
self.assertEqual(user.last_name, "Kooyman")
- def test_notification_settings_with_cms_page_published(self):
+ @requests_mock.Mocker()
+ def test_notification_settings_with_cms_page_published(self, m):
"""
Assert that notification settings can be changed via the necessary-fields form
if the corresponding CMS pages are published. Fields corresponding to unpublished
pages should not be present.
"""
+ MockAPIReadPatchData.setUpServices()
+ mock_api_data = MockAPIReadPatchData().install_mocks(m)
+
+ config = SiteConfiguration.get_solo()
+ config.enable_notification_channel_choice = True
+ config.save()
+
+ # reset noise from signals
+ m.reset_mock()
+ self.clearTimelineLogs()
+
cms_tools.create_apphook_page(
CollaborateApphook,
parent_page=self.homepage,
@@ -256,7 +273,7 @@ def test_notification_settings_with_cms_page_published(self):
url = f"{url}?{urlencode(params)}"
data = {
- "auth_name": "533458225",
+ "auth_name": mock_api_data.user.bsn,
"auth_pass": "bar",
}
@@ -269,6 +286,9 @@ def test_notification_settings_with_cms_page_published(self):
self.assertNotIn("messages_notifications", necessary_form.fields)
necessary_form["plans_notifications"] = False
+ necessary_form[
+ "case_notification_channel"
+ ] = NotificationChannelChoice.digital_only
necessary_form.submit()
user = User.objects.get(bsn=data["auth_name"])
@@ -276,6 +296,23 @@ def test_notification_settings_with_cms_page_published(self):
self.assertEqual(user.cases_notifications, True)
self.assertEqual(user.messages_notifications, True)
self.assertEqual(user.plans_notifications, False)
+ self.assertEqual(
+ user.case_notification_channel, NotificationChannelChoice.digital_only
+ )
+
+ # check klant api update
+ self.assertTrue(mock_api_data.matchers[0].called)
+ klant_patch_data = mock_api_data.matchers[1].request_history[0].json()
+ self.assertEqual(
+ klant_patch_data,
+ {
+ "toestemmingZaakNotificatiesAlleenDigitaal": True,
+ },
+ )
+ # only check logs for klant api update
+ dump = self.getTimelineLogDump()
+ msg = "patched klant from user profile edit with fields: toestemmingZaakNotificatiesAlleenDigitaal"
+ assert msg in dump
@requests_mock.Mocker()
def test_partial_response_from_haalcentraal_when_digid_and_brp(self, m):
@@ -1461,6 +1498,10 @@ def test_any_page_for_digid_user_redirect_to_necessary_fields(self):
self.assertRedirects(response, redirect.url)
def test_submit_without_invite(self):
+ config = SiteConfiguration.get_solo()
+ config.enable_notification_channel_choice = True
+ config.save()
+
user = UserFactory(
first_name="",
last_name="",
@@ -1471,9 +1512,12 @@ def test_submit_without_invite(self):
response = self.app.get(self.url, user=user)
form = response.forms["necessary-form"]
+ from open_inwoner.accounts.choices import NotificationChannelChoice
+
form["email"] = "john@smith.com"
form["first_name"] = "John"
form["last_name"] = "Smith"
+ form["case_notification_channel"] = NotificationChannelChoice.digital_only
response = form.submit()
diff --git a/src/open_inwoner/accounts/tests/test_profile_views.py b/src/open_inwoner/accounts/tests/test_profile_views.py
index f29018b66c..3c5c4a8330 100644
--- a/src/open_inwoner/accounts/tests/test_profile_views.py
+++ b/src/open_inwoner/accounts/tests/test_profile_views.py
@@ -14,8 +14,9 @@
from pyquery import PyQuery as PQ
from webtest import Upload
-from open_inwoner.accounts.choices import StatusChoices
+from open_inwoner.accounts.choices import NotificationChannelChoice, StatusChoices
from open_inwoner.cms.profile.cms_appconfig import ProfileConfig
+from open_inwoner.configurations.models import SiteConfiguration
from open_inwoner.haalcentraal.tests.mixins import HaalCentraalMixin
from open_inwoner.laposta.models import LapostaConfig
from open_inwoner.laposta.tests.factories import LapostaListFactory, MemberFactory
@@ -961,7 +962,7 @@ def test_preselected_values(self):
@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls")
@patch("open_inwoner.cms.utils.page_display._is_published", return_value=True)
-class EditNotificationsTests(WebTest):
+class EditNotificationsTests(AssertTimelineLogMixin, WebTest):
def setUp(self):
self.url = reverse("profile:notifications")
self.user = UserFactory()
@@ -993,6 +994,10 @@ def test_disabling_notification_is_saved(self, mock_page_display):
self.assertTrue(self.user.cases_notifications)
self.assertFalse(self.user.messages_notifications)
self.assertTrue(self.user.plans_notifications)
+ self.assertEqual(
+ self.user.case_notification_channel,
+ NotificationChannelChoice.digital_and_post,
+ )
def test_cases_notifications_is_accessible_when_digid_user(self, mock_page_display):
self.user.login_type = LoginTypeChoices.digid
@@ -1002,6 +1007,54 @@ def test_cases_notifications_is_accessible_when_digid_user(self, mock_page_displ
self.assertIn("cases_notifications", form.fields)
+ def test_notification_channel_not_accessible_when_disabled(self, mock_page_display):
+ response = self.app.get(self.url, user=self.user)
+ form = response.forms["change-notifications"]
+
+ # choice of notification channel is disabled by default
+ self.assertNotIn("case_notification_channel_choice", form.fields)
+
+ @requests_mock.Mocker()
+ def test_notification_channel_edit(self, mock_page_display, m):
+ MockAPIReadPatchData.setUpServices()
+ data = MockAPIReadPatchData().install_mocks(m)
+
+ config = SiteConfiguration.get_solo()
+ config.enable_notification_channel_choice = True
+ config.save()
+
+ # reset noise from signals
+ m.reset_mock()
+ self.clearTimelineLogs()
+
+ self.user.bsn = data.user.bsn
+ self.user.save()
+
+ response = self.app.get(self.url, user=self.user)
+ form = response.forms["change-notifications"]
+ form["case_notification_channel"] = NotificationChannelChoice.digital_only
+ form.submit()
+
+ # check user
+ self.user.refresh_from_db()
+ self.assertEqual(
+ self.user.case_notification_channel, NotificationChannelChoice.digital_only
+ )
+
+ # check klant api update
+ self.assertTrue(data.matchers[0].called)
+ klant_patch_data = data.matchers[1].request_history[0].json()
+ self.assertEqual(
+ klant_patch_data,
+ {
+ "toestemmingZaakNotificatiesAlleenDigitaal": True,
+ },
+ )
+ self.assertTimelineLog("retrieved klant for user")
+ self.assertTimelineLog(
+ "patched klant from user profile edit with fields: toestemmingZaakNotificatiesAlleenDigitaal"
+ )
+
@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls")
class NotificationsDisplayTests(WebTest):
diff --git a/src/open_inwoner/accounts/views/auth.py b/src/open_inwoner/accounts/views/auth.py
index 55f8578beb..3051c01457 100644
--- a/src/open_inwoner/accounts/views/auth.py
+++ b/src/open_inwoner/accounts/views/auth.py
@@ -1,3 +1,5 @@
+import logging
+
from django.conf import settings
from django.contrib import auth, messages
from django.contrib.auth.mixins import UserPassesTestMixin
@@ -31,6 +33,8 @@
from ..choices import LoginTypeChoices
from ..forms import CustomPasswordResetForm
+logger = logging.getLogger(__name__)
+
class LogPasswordChangeView(UserPassesTestMixin, LogMixin, PasswordChangeView):
def test_func(self):
diff --git a/src/open_inwoner/accounts/views/mixins.py b/src/open_inwoner/accounts/views/mixins.py
new file mode 100644
index 0000000000..3892d04a0e
--- /dev/null
+++ b/src/open_inwoner/accounts/views/mixins.py
@@ -0,0 +1,22 @@
+import logging
+
+from open_inwoner.openklant.clients import build_klanten_client
+from open_inwoner.openklant.wrap import get_fetch_parameters
+
+logger = logging.getLogger(__name__)
+
+
+class KlantenAPIMixin:
+ def patch_klant(self, update_data: dict):
+ if update_data and (client := build_klanten_client()):
+ klant = client.retrieve_klant(**get_fetch_parameters(self.request))
+ if not klant:
+ logger.error("Failed to retrieve klant for user %s", self.request.user)
+ return
+
+ self.log_system_action("retrieved klant for user", user=self.request.user)
+ client.partial_update_klant(klant, update_data)
+ self.log_system_action(
+ f"patched klant from user profile edit with fields: {', '.join(sorted(update_data.keys()))}",
+ user=self.request.user,
+ )
diff --git a/src/open_inwoner/accounts/views/profile.py b/src/open_inwoner/accounts/views/profile.py
index 7004540f6d..954c00057d 100644
--- a/src/open_inwoner/accounts/views/profile.py
+++ b/src/open_inwoner/accounts/views/profile.py
@@ -18,17 +18,17 @@
from open_inwoner.accounts.choices import (
ContactTypeChoices,
LoginTypeChoices,
+ NotificationChannelChoice,
StatusChoices,
)
from open_inwoner.cms.utils.page_display import (
benefits_page_is_published,
inbox_page_is_published,
)
+from open_inwoner.configurations.models import SiteConfiguration
from open_inwoner.haalcentraal.utils import fetch_brp
from open_inwoner.laposta.forms import NewsletterSubscriptionForm
from open_inwoner.laposta.models import LapostaConfig
-from open_inwoner.openklant.clients import build_klanten_client
-from open_inwoner.openklant.wrap import get_fetch_parameters
from open_inwoner.plans.models import Plan
from open_inwoner.qmatic.client import NoServiceConfigured, QmaticClient
from open_inwoner.questionnaire.models import QuestionnaireStep
@@ -36,6 +36,7 @@
from ..forms import BrpUserForm, CategoriesForm, UserForm, UserNotificationsForm
from ..models import Action, User
+from .mixins import KlantenAPIMixin
logger = logging.getLogger(__name__)
@@ -201,6 +202,7 @@ class EditProfileView(
LogMixin,
LoginRequiredMixin,
CommonPageMixin,
+ KlantenAPIMixin,
BaseBreadcrumbMixin,
UpdateView,
):
@@ -222,17 +224,13 @@ def get_object(self):
def form_valid(self, form):
form.save()
- self.update_klant_api({k: form.cleaned_data[k] for k in form.changed_data})
+ self.update_klant({k: form.cleaned_data[k] for k in form.changed_data})
messages.success(self.request, _("Uw wijzigingen zijn opgeslagen"))
self.log_change(self.get_object(), _("profile was modified"))
return HttpResponseRedirect(self.get_success_url())
- def update_klant_api(self, user_form_data: dict):
- user: User = self.request.user
- if not user.bsn and not user.kvk:
- return
-
+ def update_klant(self, user_form_data: dict):
field_mapping = {
"emailadres": "email",
"telefoonnummer": "phonenumber",
@@ -242,20 +240,7 @@ def update_klant_api(self, user_form_data: dict):
for api_name, local_name in field_mapping.items()
if user_form_data.get(local_name)
}
- if update_data:
- if client := build_klanten_client():
- klant = client.retrieve_klant(**get_fetch_parameters(self.request))
-
- if klant:
- self.log_system_action(
- "retrieved klant for user", user=self.request.user
- )
- client.partial_update_klant(klant, update_data)
- if klant:
- self.log_system_action(
- f"patched klant from user profile edit with fields: {', '.join(sorted(update_data.keys()))}",
- user=self.request.user,
- )
+ self.patch_klant(update_data)
def get_form_class(self):
user = self.request.user
@@ -318,7 +303,12 @@ def get_brp_data(self):
class MyNotificationsView(
- LogMixin, LoginRequiredMixin, CommonPageMixin, BaseBreadcrumbMixin, UpdateView
+ LogMixin,
+ LoginRequiredMixin,
+ CommonPageMixin,
+ KlantenAPIMixin,
+ BaseBreadcrumbMixin,
+ UpdateView,
):
template_name = "pages/profile/notifications.html"
model = User
@@ -342,10 +332,28 @@ def get_form_kwargs(self):
def form_valid(self, form):
form.save()
+
+ self.update_klant(
+ user_form_data={k: form.cleaned_data[k] for k in form.changed_data}
+ )
+
messages.success(self.request, _("Uw wijzigingen zijn opgeslagen"))
self.log_change(self.object, _("users notifications were modified"))
return HttpResponseRedirect(self.get_success_url())
+ def update_klant(self, user_form_data: dict):
+ config = SiteConfiguration.get_solo()
+ if not config.enable_notification_channel_choice:
+ return
+
+ if notification_channel := user_form_data.get("case_notification_channel"):
+ self.patch_klant(
+ update_data={
+ "toestemmingZaakNotificatiesAlleenDigitaal": notification_channel
+ == NotificationChannelChoice.digital_only
+ }
+ )
+
class UserAppointmentsView(
LogMixin,
diff --git a/src/open_inwoner/accounts/views/registration.py b/src/open_inwoner/accounts/views/registration.py
index 091fb6f5c6..8372422def 100644
--- a/src/open_inwoner/accounts/views/registration.py
+++ b/src/open_inwoner/accounts/views/registration.py
@@ -15,6 +15,9 @@
OpenIDConnectDigiDConfig,
OpenIDConnectEHerkenningConfig,
)
+from open_inwoner.accounts.choices import NotificationChannelChoice
+from open_inwoner.accounts.views.mixins import KlantenAPIMixin
+from open_inwoner.configurations.models import SiteConfiguration
from open_inwoner.utils.hash import generate_email_from_string
from open_inwoner.utils.views import CommonPageMixin, LogMixin
@@ -135,7 +138,13 @@ def get(self, request, *args, **kwargs):
return super().get(self, request, *args, **kwargs)
-class NecessaryFieldsUserView(LogMixin, LoginRequiredMixin, InviteMixin, UpdateView):
+class NecessaryFieldsUserView(
+ LogMixin,
+ LoginRequiredMixin,
+ KlantenAPIMixin,
+ InviteMixin,
+ UpdateView,
+):
model = User
form_class = NecessaryUserForm
template_name = "accounts/registration_necessary.html"
@@ -160,6 +169,8 @@ def get_form_kwargs(self):
def form_valid(self, form):
user = form.save()
+ self.update_klant({k: form.cleaned_data[k] for k in form.changed_data})
+
invite = form.cleaned_data["invite"]
if invite:
self.add_invitee(invite, user)
@@ -182,6 +193,19 @@ def get_initial(self):
return initial
+ def update_klant(self, user_form_data: dict):
+ config = SiteConfiguration.get_solo()
+ if not config.enable_notification_channel_choice:
+ return
+
+ if notification_channel := user_form_data.get("case_notification_channel"):
+ self.patch_klant(
+ update_data={
+ "toestemmingZaakNotificatiesAlleenDigitaal": notification_channel
+ == NotificationChannelChoice.digital_only
+ }
+ )
+
class EmailVerificationUserView(LogMixin, LoginRequiredMixin, TemplateView):
model = User
diff --git a/src/open_inwoner/accounts/views/signals.py b/src/open_inwoner/accounts/views/signals.py
new file mode 100644
index 0000000000..9214636d2f
--- /dev/null
+++ b/src/open_inwoner/accounts/views/signals.py
@@ -0,0 +1,33 @@
+import logging
+
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from open_inwoner.accounts.models import User
+from open_inwoner.openklant.clients import build_klanten_client
+
+logger = logging.getLogger(__name__)
+
+
+@receiver(post_save, sender=User)
+def create_klant_for_new_user(
+ sender: type, instance: User, created: bool, **kwargs
+) -> None:
+ if not created:
+ return
+
+ user = instance
+
+ if not user.bsn:
+ logger.info("Did not create klant for user %s because of missing bsn", user)
+ return
+
+ if not (client := build_klanten_client()):
+ logger.warning("Failed to create klanten client for new user %s", user)
+ return
+
+ if not (klant := client.create_klant(user_bsn=user.bsn)):
+ logger.error("Failed to create klant for new user %s", user)
+ return
+
+ logger.info("Created klant %s for new user %s", klant, user)
diff --git a/src/open_inwoner/components/templates/components/Form/ChoiceRadioStacked.html b/src/open_inwoner/components/templates/components/Form/ChoiceRadioStacked.html
new file mode 100644
index 0000000000..e1227ec92d
--- /dev/null
+++ b/src/open_inwoner/components/templates/components/Form/ChoiceRadioStacked.html
@@ -0,0 +1,20 @@
+{% load l10n form_tags icon_tags %}
+
+
+{% spaceless %}
+
+{% if icon_class %}
+{% icon icon_class %}
+{% endif %}
+
+{% initial_match as checked %}
+
+
+{% endspaceless %}
diff --git a/src/open_inwoner/components/templatetags/form_tags.py b/src/open_inwoner/components/templatetags/form_tags.py
index e7213b31bf..91541cc9e9 100644
--- a/src/open_inwoner/components/templatetags/form_tags.py
+++ b/src/open_inwoner/components/templatetags/form_tags.py
@@ -263,6 +263,26 @@ def choice_radio(choice, **kwargs):
return {**kwargs, "choice": choice}
+@register.inclusion_tag("components/Form/ChoiceRadioStacked.html")
+def choice_radio_stacked(choice, **kwargs):
+ """
+ Display radio input rendered from a choice field.
+
+ Args:
+ choice: the choice to be rendered
+ name: the name of the form field
+ data: the value of a form field field
+ index: the index of a for-loop when looping over choices
+ initial: the initial value of the field
+ icon_class: the icon to be displayed at the top of the
+ radio stack
+
+ Usage:
+ {% choice_radio_stacked choice=choice name=field.name ... icon_class=choice.1|get_icon_class %}
+ """
+ return {**kwargs, "choice": choice}
+
+
@register.inclusion_tag("components/Form/Input.html")
def input(field, **kwargs):
"""
@@ -433,9 +453,9 @@ def field_as_widget(field, class_string, form_id):
@register.simple_tag(takes_context=True)
def initial_match(context):
- initial = context.get("initial")
- choice = context.get("choice")
- name = context.get("name")
+ initial = context.get("initial", None)
+ choice = context.get("choice", None)
+ name = context.get("name", None)
return initial.get(name) == choice[0]
@@ -459,3 +479,12 @@ def render(self, context):
corrected_kwargs["classes"] = get_form_classes(**corrected_kwargs)
rendered = render_to_string("components/Form/Form.html", corrected_kwargs)
return rendered
+
+
+@register.filter
+def get_icon_class(key: str) -> str:
+ mapping = {
+ "Alleen digitaal": "computer",
+ "Digitaal en per brief": "mail",
+ }
+ return mapping.get(key, None)
diff --git a/src/open_inwoner/configurations/admin.py b/src/open_inwoner/configurations/admin.py
index 0c94469aa1..1a587dbf0f 100644
--- a/src/open_inwoner/configurations/admin.py
+++ b/src/open_inwoner/configurations/admin.py
@@ -229,7 +229,7 @@ class SiteConfigurationAdmin(OrderedInlineModelAdminMixin, SingletonModelAdmin):
},
),
(
- _("Emails"),
+ _("Notifications"),
{
"fields": (
"notifications_messages_enabled",
diff --git a/src/open_inwoner/configurations/migrations/0070_siteconfiguration_enable_notification_channels.py b/src/open_inwoner/configurations/migrations/0070_siteconfiguration_enable_notification_channels.py
new file mode 100644
index 0000000000..187dfb0c75
--- /dev/null
+++ b/src/open_inwoner/configurations/migrations/0070_siteconfiguration_enable_notification_channels.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.15 on 2024-08-14 13:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("configurations", "0069_merge_20240724_0945"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="siteconfiguration",
+ name="enable_notification_channel_choice",
+ field=models.BooleanField(
+ default=False,
+ help_text="Give users the option to choose how they want to receive notifications (digital and post or digital only)",
+ verbose_name="Enable choice of notification channel",
+ ),
+ ),
+ ]
diff --git a/src/open_inwoner/configurations/models.py b/src/open_inwoner/configurations/models.py
index b7606f6f0c..9644e47b1e 100644
--- a/src/open_inwoner/configurations/models.py
+++ b/src/open_inwoner/configurations/models.py
@@ -359,6 +359,14 @@ class SiteConfiguration(SingletonModel):
)
# email notifications
+ enable_notification_channel_choice = models.BooleanField(
+ verbose_name=_("Enable choice of notification channel"),
+ default=False,
+ help_text=_(
+ "Give users the option to choose how they want to receive notifications "
+ "(digital and post or digital only)"
+ ),
+ )
notifications_messages_enabled = models.BooleanField(
verbose_name=_("User notifications for messages"),
default=True,
diff --git a/src/open_inwoner/openklant/api_models.py b/src/open_inwoner/openklant/api_models.py
index 1b2da63eb0..c4411cd10c 100644
--- a/src/open_inwoner/openklant/api_models.py
+++ b/src/open_inwoner/openklant/api_models.py
@@ -31,6 +31,7 @@ class Klant(ZGWModel):
achternaam: str = ""
telefoonnummer: str = ""
emailadres: str = ""
+ toestemmingZaakNotificatiesAlleenDigitaal: bool | None = None
def get_name_display(self):
return " ".join(
diff --git a/src/open_inwoner/scss/components/Form/Radio.scss b/src/open_inwoner/scss/components/Form/Radio.scss
index f2245d7f1a..8186a59e1c 100644
--- a/src/open_inwoner/scss/components/Form/Radio.scss
+++ b/src/open_inwoner/scss/components/Form/Radio.scss
@@ -45,3 +45,25 @@
content: '\e876';
}
}
+
+.radios {
+ &--spaced {
+ display: flex;
+ justify-content: space-evenly;
+ }
+}
+
+.radio-group {
+ padding: var(--spacing-large);
+
+ &--stacked {
+ display: inline-block;
+ text-align: center;
+ }
+ .radio__label {
+ display: block;
+ }
+ .radio__input {
+ display: inline-block;
+ }
+}
diff --git a/src/open_inwoner/templates/pages/profile/notifications.html b/src/open_inwoner/templates/pages/profile/notifications.html
index 5f06003e7c..f6fb18f234 100644
--- a/src/open_inwoner/templates/pages/profile/notifications.html
+++ b/src/open_inwoner/templates/pages/profile/notifications.html
@@ -1,24 +1,22 @@
{% extends 'master.html' %}
-{% load i18n l10n grid_tags form_tags icon_tags button_tags %}
+{% load i18n l10n grid_tags form_tags icon_tags button_tags icon_tags %}
{% block content %}
{% render_grid %}
{% render_column start=5 span=5 %}
- {% trans "Ontvang berichten over" %}
+ {% trans "Notificatie voorkeuren" %}
-
- {% trans "Kies voor welk onderwerp je meldingen wilt ontvangen" %}
-