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 1267a46d44..043c9299bd 100644
--- a/src/open_inwoner/accounts/tests/test_profile_views.py
+++ b/src/open_inwoner/accounts/tests/test_profile_views.py
@@ -990,6 +990,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..539f3670e3
--- /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 update_klanten_api(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..832e38b2a4 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_klanten_api({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_klanten_api(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,
- )
+ super().update_klanten_api(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,35 +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.sync_notifications_with_klanten_api(
+ 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 sync_notifications_with_klanten_api(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,
- )
+ super().update_klanten_api(update_data)
class UserAppointmentsView(
diff --git a/src/open_inwoner/accounts/views/registration.py b/src/open_inwoner/accounts/views/registration.py
index fb3344e33c..4c932e4d92 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,9 @@ 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.sync_notifications_with_klanten_api(
+ {k: form.cleaned_data[k] for k in form.changed_data}
+ )
invite = form.cleaned_data["invite"]
if invite:
@@ -187,29 +194,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 sync_notifications_with_klanten_api(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,
- )
+ super().update_klanten_api(update_data)
class EmailVerificationUserView(LogMixin, LoginRequiredMixin, TemplateView):
diff --git a/src/open_inwoner/accounts/views/signals.py b/src/open_inwoner/accounts/views/signals.py
new file mode 100644
index 0000000000..473675daf4
--- /dev/null
+++ b/src/open_inwoner/accounts/views/signals.py
@@ -0,0 +1,26 @@
+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 (client := build_klanten_client()) and (
+ klant := client.create_klant(user_bsn=user.bsn)
+ ):
+ logger.info("Created klant %s for new user %s", klant, user)
+ else:
+ logger.error("Failed to create klant for new user %s", 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(