From 2996e666cd1e840cecc07d747aea1388081cbdb2 Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Tue, 6 Aug 2024 11:13:22 +0200 Subject: [PATCH 1/5] [#2638] Update profile with choice of notification channel for cases - user can edit the communication channel (post + digital or digital only) for notifications about cases in the communication section of their profile --- src/open_inwoner/accounts/choices.py | 5 +++ src/open_inwoner/accounts/forms.py | 1 + .../0076_user_case_notification_channel.py | 25 +++++++++++ src/open_inwoner/accounts/models.py | 13 +++++- .../accounts/tests/test_profile_views.py | 41 ++++++++++++++++++- src/open_inwoner/accounts/views/profile.py | 28 +++++++++++++ .../components/templatetags/form_tags.py | 20 +++++++-- src/open_inwoner/openklant/api_models.py | 1 + src/open_inwoner/openklant/tests/data.py | 1 + .../pages/profile/notifications.html | 27 ++++++------ 10 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 src/open_inwoner/accounts/migrations/0076_user_case_notification_channel.py 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..0e1b01024b 100644 --- a/src/open_inwoner/accounts/forms.py +++ b/src/open_inwoner/accounts/forms.py @@ -322,6 +322,7 @@ class Meta: "cases_notifications", "messages_notifications", "plans_notifications", + "case_notification_channel", ) def __init__(self, user, *args, **kwargs) -> None: 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/tests/test_profile_views.py b/src/open_inwoner/accounts/tests/test_profile_views.py index f29018b66c..fd98cce726 100644 --- a/src/open_inwoner/accounts/tests/test_profile_views.py +++ b/src/open_inwoner/accounts/tests/test_profile_views.py @@ -14,7 +14,7 @@ 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.haalcentraal.tests.mixins import HaalCentraalMixin from open_inwoner.laposta.models import LapostaConfig @@ -961,7 +961,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() @@ -1002,6 +1002,43 @@ def test_cases_notifications_is_accessible_when_digid_user(self, mock_page_displ self.assertIn("cases_notifications", form.fields) + @requests_mock.Mocker() + def test_notification_channel_edit(self, mock_page_display, m): + MockAPIReadPatchData.setUpServices() + data = MockAPIReadPatchData().install_mocks(m) + + # 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/profile.py b/src/open_inwoner/accounts/views/profile.py index 7004540f6d..19f6a8766c 100644 --- a/src/open_inwoner/accounts/views/profile.py +++ b/src/open_inwoner/accounts/views/profile.py @@ -18,6 +18,7 @@ from open_inwoner.accounts.choices import ( ContactTypeChoices, LoginTypeChoices, + NotificationChannelChoice, StatusChoices, ) from open_inwoner.cms.utils.page_display import ( @@ -342,10 +343,37 @@ def get_form_kwargs(self): def form_valid(self, form): form.save() + + self.update_klant_api({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_api(self, user_form_data: dict): + user: User = self.request.user + if not user.bsn and not user.kvk: + return + + update_data = {} + if notification_channel := user_form_data.get("case_notification_channel"): + update_data = { + "toestemmingZaakNotificatiesAlleenDigitaal": notification_channel + == NotificationChannelChoice.digital_only + } + + if update_data and (client := build_klanten_client()): + klant = client.retrieve_klant(**get_fetch_parameters(self.request)) + if not klant: + 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, + ) + class UserAppointmentsView( LogMixin, diff --git a/src/open_inwoner/components/templatetags/form_tags.py b/src/open_inwoner/components/templatetags/form_tags.py index e7213b31bf..1fbd9f3c12 100644 --- a/src/open_inwoner/components/templatetags/form_tags.py +++ b/src/open_inwoner/components/templatetags/form_tags.py @@ -263,6 +263,20 @@ def choice_radio(choice, **kwargs): return {**kwargs, "choice": choice} +@register.inclusion_tag(WIDGET_TEMPLATES["RADIO"]) +def choice_radio_multiple(field, **kwargs): + """ + Display multiple radio inputs that are rendered from a choice field. + + Usage: + {% choice_radio_multiple form.radio_field %} + + Variables: + + field: The field that needs to be rendered. + """ + return {**kwargs, "field": field} + + @register.inclusion_tag("components/Form/Input.html") def input(field, **kwargs): """ @@ -433,9 +447,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] diff --git a/src/open_inwoner/openklant/api_models.py b/src/open_inwoner/openklant/api_models.py index 1b2da63eb0..33161dc39f 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 = False def get_name_display(self): return " ".join( diff --git a/src/open_inwoner/openklant/tests/data.py b/src/open_inwoner/openklant/tests/data.py index b18f25d78e..1bd136f91e 100644 --- a/src/open_inwoner/openklant/tests/data.py +++ b/src/open_inwoner/openklant/tests/data.py @@ -40,6 +40,7 @@ def setUpServices(cls): class MockAPIReadPatchData(MockAPIData): def __init__(self): self.user = DigidUserFactory( + # bsn = "123456789", email="old@example.com", phonenumber="0100000000", ) diff --git a/src/open_inwoner/templates/pages/profile/notifications.html b/src/open_inwoner/templates/pages/profile/notifications.html index 5f06003e7c..99fc1bb9b3 100644 --- a/src/open_inwoner/templates/pages/profile/notifications.html +++ b/src/open_inwoner/templates/pages/profile/notifications.html @@ -5,20 +5,18 @@ {% 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)" %}

- {% endif %} +
+ +

{% trans "How do you want to receive notifications about cases?" %}

+ {% choice_radio_multiple form.case_notification_channel %}
{% button text=_("Sla wijzigingen op") primary=True type="submit" form_id="change-notifications" %} From 51890afe4d962ee1a4bb7e6bce49bc0eaec3823f Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Thu, 8 Aug 2024 09:37:42 +0200 Subject: [PATCH 2/5] [#2638] Update registration with choice of zaken notification channel --- src/open_inwoner/accounts/forms.py | 1 + .../accounts/registration_necessary.html | 3 ++ src/open_inwoner/accounts/tests/test_auth.py | 42 +++++++++++++++++-- src/open_inwoner/accounts/views/auth.py | 34 +++++++++++++++ .../accounts/views/registration.py | 29 +++++++++++++ src/open_inwoner/openklant/tests/data.py | 1 - 6 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/open_inwoner/accounts/forms.py b/src/open_inwoner/accounts/forms.py index 0e1b01024b..4ac024ed00 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): diff --git a/src/open_inwoner/accounts/templates/accounts/registration_necessary.html b/src/open_inwoner/accounts/templates/accounts/registration_necessary.html index a074ca8575..e012d9f8e1 100644 --- a/src/open_inwoner/accounts/templates/accounts/registration_necessary.html +++ b/src/open_inwoner/accounts/templates/accounts/registration_necessary.html @@ -69,6 +69,9 @@

{% trans "Notification preferences" %}

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

+

{% trans "How do you want to receive notifications about cases?" %}

+ {% choice_radio_multiple form.case_notification_channel %} + {% 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..43f3f6619c 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,20 @@ 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) + + # reset noise from signals + m.reset_mock() + self.clearTimelineLogs() + cms_tools.create_apphook_page( CollaborateApphook, parent_page=self.homepage, @@ -256,7 +269,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 +282,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 +292,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): @@ -1471,9 +1504,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/views/auth.py b/src/open_inwoner/accounts/views/auth.py index 55f8578beb..2d8c3d7c01 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 @@ -16,6 +18,7 @@ from digid_eherkenning.views.base import get_redirect_url from digid_eherkenning.views.digid import DigiDAssertionConsumerServiceView from digid_eherkenning.views.eherkenning import eHerkenningAssertionConsumerServiceView +from onelogin.saml2.utils import OneLogin_Saml2_ValidationError from digid_eherkenning_oidc_generics.views import ( eHerkenningOIDCAuthenticationCallbackView, @@ -24,6 +27,7 @@ from eherkenning.mock.views.eherkenning import ( eHerkenningAssertionConsumerServiceMockView, ) +from open_inwoner.openklant.clients import build_klanten_client from open_inwoner.openklant.models import OpenKlantConfig from open_inwoner.openzaak.models import OpenZaakConfig from open_inwoner.utils.views import LogMixin @@ -31,6 +35,8 @@ from ..choices import LoginTypeChoices from ..forms import CustomPasswordResetForm +logger = logging.getLogger(__name__) + class LogPasswordChangeView(UserPassesTestMixin, LogMixin, PasswordChangeView): def test_func(self): @@ -103,6 +109,34 @@ def get_success_url(self): class CustomDigiDAssertionConsumerServiceView(DigiDAssertionConsumerServiceView): + def get(self, request): + errors = [] + user = auth.authenticate( + request=request, + digid=True, + saml_art=request.GET.get("SAMLart"), + errors=errors, + ) + if user is None: + error_code = getattr(errors[0], "code", "") if errors else "" + error_type = ( + "cancelled" + if error_code == OneLogin_Saml2_ValidationError.STATUS_CODE_AUTHNFAILED + else "default" + ) + messages.error(request, self.error_messages[error_type]) + login_url = self.get_login_url(error_type=error_type) + return HttpResponseRedirect(login_url) + + if (client := build_klanten_client()) and ( + klant := client.create_klant(user_bsn=user.bsn) + ): + logger.info("Created klant %s for new user %s", klant, user) + + auth.login(request, user) + + return HttpResponseRedirect(self.get_success_url()) + def get_login_url(self, **kwargs): invite_url = self.request.session.get("invite_url") next_url = self.request.GET.get("RelayState") diff --git a/src/open_inwoner/accounts/views/registration.py b/src/open_inwoner/accounts/views/registration.py index 091fb6f5c6..fb3344e33c 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.openklant.clients import build_klanten_client +from open_inwoner.openklant.wrap import get_fetch_parameters from open_inwoner.utils.hash import generate_email_from_string from open_inwoner.utils.views import CommonPageMixin, LogMixin @@ -160,6 +163,8 @@ def get_form_kwargs(self): def form_valid(self, form): user = form.save() + self.update_klant_api({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 +187,30 @@ def get_initial(self): return initial + def update_klant_api(self, user_form_data: dict): + user: User = self.request.user + if not user.bsn and not user.kvk: + return + + update_data = {} + if notification_channel := user_form_data.get("case_notification_channel"): + update_data = { + "toestemmingZaakNotificatiesAlleenDigitaal": notification_channel + == NotificationChannelChoice.digital_only + } + + if update_data and (client := build_klanten_client()): + klant = client.retrieve_klant(**get_fetch_parameters(self.request)) + if not klant: + 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, + ) + class EmailVerificationUserView(LogMixin, LoginRequiredMixin, TemplateView): model = User diff --git a/src/open_inwoner/openklant/tests/data.py b/src/open_inwoner/openklant/tests/data.py index 1bd136f91e..b18f25d78e 100644 --- a/src/open_inwoner/openklant/tests/data.py +++ b/src/open_inwoner/openklant/tests/data.py @@ -40,7 +40,6 @@ def setUpServices(cls): class MockAPIReadPatchData(MockAPIData): def __init__(self): self.user = DigidUserFactory( - # bsn = "123456789", email="old@example.com", phonenumber="0100000000", ) From a1fe1d08f650ad7982f9ae8270b75e01ce89f724 Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Fri, 9 Aug 2024 13:02:06 +0200 Subject: [PATCH 3/5] [#2638] Update design for radio group --- .../components/Form/ChoiceRadioStacked.html | 20 ++++++++++++++++ .../components/templatetags/form_tags.py | 23 +++++++++++++++++++ .../scss/components/Form/Radio.scss | 22 ++++++++++++++++++ .../pages/profile/notifications.html | 13 +++++++++-- 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/open_inwoner/components/templates/components/Form/ChoiceRadioStacked.html 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 1fbd9f3c12..df59c97404 100644 --- a/src/open_inwoner/components/templatetags/form_tags.py +++ b/src/open_inwoner/components/templatetags/form_tags.py @@ -263,6 +263,20 @@ def choice_radio(choice, **kwargs): return {**kwargs, "choice": choice} +@register.inclusion_tag("components/Form/ChoiceRadioStacked.html") +def choice_radio_stacked(choice, **kwargs): + """ + Displaying a radio input that is rendered from a choice field. + + Usage: + {% choice_radio form.radio_field %} + + Variables: + + choice: The choice that needs to be rendered. + """ + return {**kwargs, "choice": choice} + + @register.inclusion_tag(WIDGET_TEMPLATES["RADIO"]) def choice_radio_multiple(field, **kwargs): """ @@ -473,3 +487,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/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 99fc1bb9b3..ed320d987f 100644 --- a/src/open_inwoner/templates/pages/profile/notifications.html +++ b/src/open_inwoner/templates/pages/profile/notifications.html @@ -1,5 +1,5 @@ {% 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 %} @@ -49,8 +49,17 @@

{% trans "Ontvang berichten over" %}

+ {# Choice of zaken notification channel #}

{% trans "How do you want to receive notifications about cases?" %}

- {% choice_radio_multiple form.case_notification_channel %} +
+ {% 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 %} +
{% button text=_("Sla wijzigingen op") primary=True type="submit" form_id="change-notifications" %} From 15c3c42e4d710308229b838191c57db2cc240d68 Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Tue, 13 Aug 2024 08:49:28 +0200 Subject: [PATCH 4/5] [#2638] Use Mixin to update klanten API when user is created --- .../accounts/registration_necessary.html | 11 +++- .../accounts/tests/test_profile_views.py | 4 ++ src/open_inwoner/accounts/views/auth.py | 30 --------- src/open_inwoner/accounts/views/mixins.py | 22 +++++++ src/open_inwoner/accounts/views/profile.py | 65 ++++++------------- .../accounts/views/registration.py | 40 +++++------- src/open_inwoner/accounts/views/signals.py | 33 ++++++++++ .../components/templatetags/form_tags.py | 30 ++++----- src/open_inwoner/openklant/api_models.py | 2 +- 9 files changed, 116 insertions(+), 121 deletions(-) create mode 100644 src/open_inwoner/accounts/views/mixins.py create mode 100644 src/open_inwoner/accounts/views/signals.py diff --git a/src/open_inwoner/accounts/templates/accounts/registration_necessary.html b/src/open_inwoner/accounts/templates/accounts/registration_necessary.html index e012d9f8e1..856b5246d9 100644 --- a/src/open_inwoner/accounts/templates/accounts/registration_necessary.html +++ b/src/open_inwoner/accounts/templates/accounts/registration_necessary.html @@ -69,8 +69,17 @@

{% trans "Notification preferences" %}

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

+ {# Choice of zaken notification channel #}

{% trans "How do you want to receive notifications about cases?" %}

- {% choice_radio_multiple form.case_notification_channel %} +
+ {% 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 %} +
{% form_actions primary_icon='east' primary_text="Voltooi registratie" fullwidth=True %} diff --git a/src/open_inwoner/accounts/tests/test_profile_views.py b/src/open_inwoner/accounts/tests/test_profile_views.py index fd98cce726..c0d6d1e075 100644 --- a/src/open_inwoner/accounts/tests/test_profile_views.py +++ b/src/open_inwoner/accounts/tests/test_profile_views.py @@ -993,6 +993,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 diff --git a/src/open_inwoner/accounts/views/auth.py b/src/open_inwoner/accounts/views/auth.py index 2d8c3d7c01..3051c01457 100644 --- a/src/open_inwoner/accounts/views/auth.py +++ b/src/open_inwoner/accounts/views/auth.py @@ -18,7 +18,6 @@ from digid_eherkenning.views.base import get_redirect_url from digid_eherkenning.views.digid import DigiDAssertionConsumerServiceView from digid_eherkenning.views.eherkenning import eHerkenningAssertionConsumerServiceView -from onelogin.saml2.utils import OneLogin_Saml2_ValidationError from digid_eherkenning_oidc_generics.views import ( eHerkenningOIDCAuthenticationCallbackView, @@ -27,7 +26,6 @@ from eherkenning.mock.views.eherkenning import ( eHerkenningAssertionConsumerServiceMockView, ) -from open_inwoner.openklant.clients import build_klanten_client from open_inwoner.openklant.models import OpenKlantConfig from open_inwoner.openzaak.models import OpenZaakConfig from open_inwoner.utils.views import LogMixin @@ -109,34 +107,6 @@ def get_success_url(self): class CustomDigiDAssertionConsumerServiceView(DigiDAssertionConsumerServiceView): - def get(self, request): - errors = [] - user = auth.authenticate( - request=request, - digid=True, - saml_art=request.GET.get("SAMLart"), - errors=errors, - ) - if user is None: - error_code = getattr(errors[0], "code", "") if errors else "" - error_type = ( - "cancelled" - if error_code == OneLogin_Saml2_ValidationError.STATUS_CODE_AUTHNFAILED - else "default" - ) - messages.error(request, self.error_messages[error_type]) - login_url = self.get_login_url(error_type=error_type) - return HttpResponseRedirect(login_url) - - if (client := build_klanten_client()) and ( - klant := client.create_klant(user_bsn=user.bsn) - ): - logger.info("Created klant %s for new user %s", klant, user) - - auth.login(request, user) - - return HttpResponseRedirect(self.get_success_url()) - def get_login_url(self, **kwargs): invite_url = self.request.session.get("invite_url") next_url = self.request.GET.get("RelayState") 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 19f6a8766c..494cf19142 100644 --- a/src/open_inwoner/accounts/views/profile.py +++ b/src/open_inwoner/accounts/views/profile.py @@ -28,8 +28,6 @@ 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 @@ -37,6 +35,7 @@ from ..forms import BrpUserForm, CategoriesForm, UserForm, UserNotificationsForm from ..models import Action, User +from .mixins import KlantenAPIMixin logger = logging.getLogger(__name__) @@ -202,6 +201,7 @@ class EditProfileView( LogMixin, LoginRequiredMixin, CommonPageMixin, + KlantenAPIMixin, BaseBreadcrumbMixin, UpdateView, ): @@ -223,17 +223,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", @@ -243,20 +239,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 @@ -319,7 +302,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 @@ -344,34 +332,21 @@ def get_form_kwargs(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( + 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_api(self, user_form_data: dict): - user: User = self.request.user - if not user.bsn and not user.kvk: - return - - update_data = {} + def update_klant(self, user_form_data: dict): if notification_channel := user_form_data.get("case_notification_channel"): - update_data = { - "toestemmingZaakNotificatiesAlleenDigitaal": notification_channel - == NotificationChannelChoice.digital_only - } - - if update_data and (client := build_klanten_client()): - klant = client.retrieve_klant(**get_fetch_parameters(self.request)) - if not klant: - 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, + self.patch_klant( + update_data={ + "toestemmingZaakNotificatiesAlleenDigitaal": notification_channel + == NotificationChannelChoice.digital_only + } ) diff --git a/src/open_inwoner/accounts/views/registration.py b/src/open_inwoner/accounts/views/registration.py index fb3344e33c..cd0f2846e5 100644 --- a/src/open_inwoner/accounts/views/registration.py +++ b/src/open_inwoner/accounts/views/registration.py @@ -16,8 +16,7 @@ OpenIDConnectEHerkenningConfig, ) from open_inwoner.accounts.choices import NotificationChannelChoice -from open_inwoner.openklant.clients import build_klanten_client -from open_inwoner.openklant.wrap import get_fetch_parameters +from open_inwoner.accounts.views.mixins import KlantenAPIMixin from open_inwoner.utils.hash import generate_email_from_string from open_inwoner.utils.views import CommonPageMixin, LogMixin @@ -138,7 +137,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" @@ -163,7 +168,7 @@ def get_form_kwargs(self): def form_valid(self, form): user = 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}) invite = form.cleaned_data["invite"] if invite: @@ -187,28 +192,13 @@ def get_initial(self): return initial - def update_klant_api(self, user_form_data: dict): - user: User = self.request.user - if not user.bsn and not user.kvk: - return - - update_data = {} + def update_klant(self, user_form_data: dict): if notification_channel := user_form_data.get("case_notification_channel"): - update_data = { - "toestemmingZaakNotificatiesAlleenDigitaal": notification_channel - == NotificationChannelChoice.digital_only - } - - if update_data and (client := build_klanten_client()): - klant = client.retrieve_klant(**get_fetch_parameters(self.request)) - if not klant: - 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, + self.patch_klant( + update_data={ + "toestemmingZaakNotificatiesAlleenDigitaal": notification_channel + == NotificationChannelChoice.digital_only + } ) 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/templatetags/form_tags.py b/src/open_inwoner/components/templatetags/form_tags.py index df59c97404..91541cc9e9 100644 --- a/src/open_inwoner/components/templatetags/form_tags.py +++ b/src/open_inwoner/components/templatetags/form_tags.py @@ -266,29 +266,21 @@ def choice_radio(choice, **kwargs): @register.inclusion_tag("components/Form/ChoiceRadioStacked.html") def choice_radio_stacked(choice, **kwargs): """ - Displaying a radio input that is rendered from a choice field. - - Usage: - {% choice_radio form.radio_field %} + Display radio input rendered from a choice field. - Variables: - + choice: The choice that needs to be rendered. - """ - return {**kwargs, "choice": choice} - - -@register.inclusion_tag(WIDGET_TEMPLATES["RADIO"]) -def choice_radio_multiple(field, **kwargs): - """ - Display multiple radio inputs that are 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_multiple form.radio_field %} - - Variables: - + field: The field that needs to be rendered. + {% choice_radio_stacked choice=choice name=field.name ... icon_class=choice.1|get_icon_class %} """ - return {**kwargs, "field": field} + return {**kwargs, "choice": choice} @register.inclusion_tag("components/Form/Input.html") diff --git a/src/open_inwoner/openklant/api_models.py b/src/open_inwoner/openklant/api_models.py index 33161dc39f..c4411cd10c 100644 --- a/src/open_inwoner/openklant/api_models.py +++ b/src/open_inwoner/openklant/api_models.py @@ -31,7 +31,7 @@ class Klant(ZGWModel): achternaam: str = "" telefoonnummer: str = "" emailadres: str = "" - toestemmingZaakNotificatiesAlleenDigitaal: bool = False + toestemmingZaakNotificatiesAlleenDigitaal: bool | None = None def get_name_display(self): return " ".join( From 204dbdba6c0df3b6af30d58f398d9ea747275312 Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Wed, 14 Aug 2024 15:56:00 +0200 Subject: [PATCH 5/5] [#2638] Make choice of notification channel optional --- src/open_inwoner/accounts/forms.py | 6 +++++ .../accounts/registration_necessary.html | 22 ++++++++++--------- src/open_inwoner/accounts/tests/test_auth.py | 8 +++++++ .../accounts/tests/test_profile_views.py | 12 ++++++++++ src/open_inwoner/accounts/views/profile.py | 5 +++++ .../accounts/views/registration.py | 5 +++++ src/open_inwoner/configurations/admin.py | 2 +- ...figuration_enable_notification_channels.py | 22 +++++++++++++++++++ src/open_inwoner/configurations/models.py | 8 +++++++ .../pages/profile/notifications.html | 22 ++++++++++--------- 10 files changed, 91 insertions(+), 21 deletions(-) create mode 100644 src/open_inwoner/configurations/migrations/0070_siteconfiguration_enable_notification_channels.py diff --git a/src/open_inwoner/accounts/forms.py b/src/open_inwoner/accounts/forms.py index 4ac024ed00..f11d221857 100644 --- a/src/open_inwoner/accounts/forms.py +++ b/src/open_inwoner/accounts/forms.py @@ -205,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 @@ -331,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/templates/accounts/registration_necessary.html b/src/open_inwoner/accounts/templates/accounts/registration_necessary.html index 856b5246d9..49a56e114a 100644 --- a/src/open_inwoner/accounts/templates/accounts/registration_necessary.html +++ b/src/open_inwoner/accounts/templates/accounts/registration_necessary.html @@ -70,16 +70,18 @@

{% trans "Notification preferences" %}

{# Choice of zaken 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 %} -
+ {% 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 %} diff --git a/src/open_inwoner/accounts/tests/test_auth.py b/src/open_inwoner/accounts/tests/test_auth.py index 43f3f6619c..44bd12d595 100644 --- a/src/open_inwoner/accounts/tests/test_auth.py +++ b/src/open_inwoner/accounts/tests/test_auth.py @@ -250,6 +250,10 @@ def test_notification_settings_with_cms_page_published(self, m): 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() @@ -1494,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="", diff --git a/src/open_inwoner/accounts/tests/test_profile_views.py b/src/open_inwoner/accounts/tests/test_profile_views.py index c0d6d1e075..3c5c4a8330 100644 --- a/src/open_inwoner/accounts/tests/test_profile_views.py +++ b/src/open_inwoner/accounts/tests/test_profile_views.py @@ -16,6 +16,7 @@ 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 @@ -1006,11 +1007,22 @@ 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() diff --git a/src/open_inwoner/accounts/views/profile.py b/src/open_inwoner/accounts/views/profile.py index 494cf19142..954c00057d 100644 --- a/src/open_inwoner/accounts/views/profile.py +++ b/src/open_inwoner/accounts/views/profile.py @@ -25,6 +25,7 @@ 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 @@ -341,6 +342,10 @@ def form_valid(self, form): 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={ diff --git a/src/open_inwoner/accounts/views/registration.py b/src/open_inwoner/accounts/views/registration.py index cd0f2846e5..8372422def 100644 --- a/src/open_inwoner/accounts/views/registration.py +++ b/src/open_inwoner/accounts/views/registration.py @@ -17,6 +17,7 @@ ) 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 @@ -193,6 +194,10 @@ 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={ 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/templates/pages/profile/notifications.html b/src/open_inwoner/templates/pages/profile/notifications.html index ed320d987f..f6fb18f234 100644 --- a/src/open_inwoner/templates/pages/profile/notifications.html +++ b/src/open_inwoner/templates/pages/profile/notifications.html @@ -50,16 +50,18 @@

{% trans "Ontvang berichten over" %}

{# Choice of zaken 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 %} -
+ {% 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 %}
{% button text=_("Sla wijzigingen op") primary=True type="submit" form_id="change-notifications" %}