Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#2638] Implement choice of zaken notification channel #1336

Merged
merged 5 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/open_inwoner/accounts/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
8 changes: 8 additions & 0 deletions src/open_inwoner/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ class Meta:
"cases_notifications",
"messages_notifications",
"plans_notifications",
"case_notification_channel",
)

def __init__(self, user, *args, **kwargs):
Expand All @@ -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
Expand Down Expand Up @@ -322,13 +326,17 @@ class Meta:
"cases_notifications",
"messages_notifications",
"plans_notifications",
"case_notification_channel",
)

def __init__(self, user, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
13 changes: 12 additions & 1 deletion src/open_inwoner/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ <h3 class="utrecht-heading-3">{% trans "Notification preferences" %}</h3>
<p class="choice-list__p">{% trans "E-mailnotificaties wanneer er actie nodig is voor een zaak (kan niet uitgeschakeld worden)" %}</p>
</div>

{# Choice of zaken notification channel #}
{% if form.case_notification_channel %}
<h3 class="utrecht-heading-4">{% trans "How do you want to receive notifications about cases?" %}</h3>
<div class="radios radios--spaced">
{% with form.case_notification_channel as field %}
{% for choice in field.field.choices %}
<div class="radio-group radio-group--stacked choice-list-multiple__item">
{% 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 %}
</div>
{% endfor %}
{% endwith %}
</div>
{% endif %}

{% form_actions primary_icon='east' primary_text="Voltooi registratie" fullwidth=True %}
</form>
{% endrender_column %}
Expand Down
50 changes: 47 additions & 3 deletions src/open_inwoner/accounts/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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",
}

Expand All @@ -269,13 +286,33 @@ 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"])

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):
Expand Down Expand Up @@ -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="",
Expand All @@ -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()

Expand Down
57 changes: 55 additions & 2 deletions src/open_inwoner/accounts/tests/test_profile_views.py
pi-sigma marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
swrichards marked this conversation as resolved.
Show resolved Hide resolved
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):
Expand Down
4 changes: 4 additions & 0 deletions src/open_inwoner/accounts/views/auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging

from django.conf import settings
from django.contrib import auth, messages
from django.contrib.auth.mixins import UserPassesTestMixin
Expand Down Expand Up @@ -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):
Expand Down
22 changes: 22 additions & 0 deletions src/open_inwoner/accounts/views/mixins.py
Original file line number Diff line number Diff line change
@@ -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:
swrichards marked this conversation as resolved.
Show resolved Hide resolved
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,
)
Loading
Loading