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" %} -

{% csrf_token %} -

Notificatie voorkeuren

- {# Start of multiple checkbox fields #} {# End of multiple checkbox fields #} - {% if siteconfig.notifications_cases_enabled %} -
- {# Info on notifications that cannot be disabled #} -
-

{% icon icon="check" outlined=True %} {% trans "Zaaknotificaties - actie is nodig" %}

-

{% trans "E-mailnotificaties wanneer er actie nodig is voor een zaak (kan niet uitgeschakeld worden)" %}

-
+
+ {# Info on notifications that cannot be disabled #} +
+

{% icon icon="check" outlined=True %} {% trans "Zaaknotificaties - actie is nodig" %}

+

{% 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 %}