diff --git a/README.rst b/README.rst index 4438021841..0e1235ba24 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Open Inwoner ================== -:Version: 1.7 +:Version: 1.8 :Source: https://github.com/maykinmedia/open-inwoner :Keywords: inwoner :PythonVersion: 3.9 diff --git a/requirements/base.txt b/requirements/base.txt index 046c8ca328..560e83a314 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -136,7 +136,6 @@ django-better-admin-arrayfield==1.4.2 django-choices==1.7.2 # via # -r requirements/base.in - # django-digid-eherkenning # mail-editor django-ckeditor==6.2.0 # via mail-editor @@ -161,7 +160,7 @@ django-csp==3.7 # via -r requirements/base.in django-csp-reports==1.8.1 # via -r requirements/base.in -django-digid-eherkenning==0.4.1 +django-digid-eherkenning==0.7.0 # via -r requirements/base.in django-elasticsearch-dsl==7.2.1 # via -r requirements/base.in @@ -232,12 +231,15 @@ django-sessionprofile==1.0 # -r requirements/base.in # django-digid-eherkenning django-simple-certmanager==1.3.0 - # via zgw-consumers + # via + # django-digid-eherkenning + # zgw-consumers django-sniplates==0.7.0 # via -r requirements/base.in django-solo==1.2.0 # via # -r requirements/base.in + # django-digid-eherkenning # django-open-forms-client # mozilla-django-oidc-db # notifications-api-common @@ -357,7 +359,9 @@ markuppy==1.14 maykin-django-two-factor-auth==2.0.4 # via -r requirements/base.in maykin-python3-saml==1.14.0.post0 - # via -r requirements/base.in + # via + # -r requirements/base.in + # django-digid-eherkenning messagebird==2.1.0 # via -r requirements/base.in mozilla-django-oidc==2.0.0 diff --git a/requirements/ci.txt b/requirements/ci.txt index 766c37d50d..706f351604 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -210,7 +210,6 @@ django-choices==1.7.2 # via # -c requirements/base.txt # -r requirements/base.txt - # django-digid-eherkenning # mail-editor django-ckeditor==6.2.0 # via @@ -249,7 +248,7 @@ django-csp-reports==1.8.1 # via # -c requirements/base.txt # -r requirements/base.txt -django-digid-eherkenning==0.4.1 +django-digid-eherkenning==0.7.0 # via # -c requirements/base.txt # -r requirements/base.txt @@ -383,6 +382,7 @@ django-simple-certmanager==1.3.0 # via # -c requirements/base.txt # -r requirements/base.txt + # django-digid-eherkenning # zgw-consumers django-sniplates==0.7.0 # via @@ -392,6 +392,7 @@ django-solo==1.2.0 # via # -c requirements/base.txt # -r requirements/base.txt + # django-digid-eherkenning # django-open-forms-client # mozilla-django-oidc-db # notifications-api-common @@ -628,6 +629,7 @@ maykin-python3-saml==1.14.0.post0 # via # -c requirements/base.txt # -r requirements/base.txt + # django-digid-eherkenning mccabe==0.6.1 # via pylint messagebird==2.1.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 7d23cc33bd..885018112b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -235,7 +235,6 @@ django-choices==1.7.2 # via # -c requirements/ci.txt # -r requirements/ci.txt - # django-digid-eherkenning # mail-editor django-ckeditor==6.2.0 # via @@ -276,7 +275,7 @@ django-csp-reports==1.8.1 # -r requirements/ci.txt django-debug-toolbar==3.2.2 # via -r requirements/dev.in -django-digid-eherkenning==0.4.1 +django-digid-eherkenning==0.7.0 # via # -c requirements/ci.txt # -r requirements/ci.txt @@ -412,6 +411,7 @@ django-simple-certmanager==1.3.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # django-digid-eherkenning # zgw-consumers django-sniplates==0.7.0 # via @@ -421,6 +421,7 @@ django-solo==1.2.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # django-digid-eherkenning # django-open-forms-client # mozilla-django-oidc-db # notifications-api-common @@ -686,6 +687,7 @@ maykin-python3-saml==1.14.0.post0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # django-digid-eherkenning mccabe==0.6.1 # via # -c requirements/ci.txt diff --git a/src/open_inwoner/accounts/admin.py b/src/open_inwoner/accounts/admin.py index 7c187cf80d..4a8207951c 100644 --- a/src/open_inwoner/accounts/admin.py +++ b/src/open_inwoner/accounts/admin.py @@ -71,16 +71,9 @@ class _UserAdmin(ImageCroppingMixin, UserAdmin): "infix", "last_name", "contact_type", - "bsn", - "rsin", "oidc_id", - "birthday", "image", "cropping", - "street", - "housenumber", - "postcode", - "city", "phonenumber", "selected_categories", ) diff --git a/src/open_inwoner/accounts/forms.py b/src/open_inwoner/accounts/forms.py index 31ba28ba04..5fe50d0ace 100644 --- a/src/open_inwoner/accounts/forms.py +++ b/src/open_inwoner/accounts/forms.py @@ -1,5 +1,3 @@ -import os - from django import forms from django.conf import settings from django.contrib.auth import authenticate @@ -12,15 +10,11 @@ from django_registration.forms import RegistrationForm from open_inwoner.configurations.models import SiteConfiguration -from open_inwoner.openzaak.models import ( - OpenZaakConfig, - ZaakTypeInformatieObjectTypeConfig, -) from open_inwoner.pdc.models.category import Category from open_inwoner.utils.forms import LimitedUploadFileField, PrivateFileWidget from open_inwoner.utils.validators import ( + CharFieldValidator, format_phone_number, - validate_charfield_entry, validate_phone_number, ) @@ -313,10 +307,10 @@ class ContactFilterForm(forms.Form): class ContactCreateForm(forms.Form): first_name = forms.CharField( - label=_("First name"), max_length=255, validators=[validate_charfield_entry] + label=_("First name"), max_length=255, validators=[CharFieldValidator()] ) last_name = forms.CharField( - label=_("Last name"), max_length=255, validators=[validate_charfield_entry] + label=_("Last name"), max_length=255, validators=[CharFieldValidator()] ) email = forms.EmailField(label=_("Email")) @@ -551,51 +545,3 @@ class Meta: def __init__(self, users, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["is_for"].queryset = User.objects.filter(pk__in=users) - - -class CaseUploadForm(forms.Form): - title = forms.CharField( - label=_("Titel document"), max_length=255, validators=[validate_charfield_entry] - ) - type = forms.ModelChoiceField( - ZaakTypeInformatieObjectTypeConfig.objects.none(), - empty_label=None, - label=_("Bestand type"), - ) - file = forms.FileField(label=_("Bestand")) - - def __init__(self, case, **kwargs): - super().__init__(**kwargs) - - if case: - self.fields[ - "type" - ].queryset = ZaakTypeInformatieObjectTypeConfig.objects.filter_enabled_for_case_type( - case.zaaktype - ) - - choices = self.fields["type"].choices - - if choices and len(choices) == 1: - self.fields["type"].initial = list(choices)[0][0].value - self.fields["type"].widget = forms.HiddenInput() - - def clean_file(self): - file = self.cleaned_data["file"] - - config = OpenZaakConfig.get_solo() - max_allowed_size = 1024**2 * config.max_upload_size - allowed_extensions = sorted(config.allowed_file_extensions) - filename, file_extension = os.path.splitext(file.name) - - if file.size > max_allowed_size: - raise ValidationError( - f"Een aangeleverd bestand dient maximaal {config.max_upload_size} MB te zijn, uw bestand is te groot." - ) - - if file_extension.lower().replace(".", "") not in allowed_extensions: - raise ValidationError( - f"Het type bestand dat u hebt geüpload is ongeldig. Geldige bestandstypen zijn: {', '.join(allowed_extensions)}" - ) - - return file diff --git a/src/open_inwoner/accounts/management/commands/deleteinactiveusers.py b/src/open_inwoner/accounts/management/commands/deleteinactiveusers.py deleted file mode 100644 index 7d87cc12e4..0000000000 --- a/src/open_inwoner/accounts/management/commands/deleteinactiveusers.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging -from datetime import date, timedelta - -from django.conf import settings -from django.contrib.auth import get_user_model -from django.core.management.base import BaseCommand - -logger = logging.getLogger(__name__) - - -class Command(BaseCommand): - help = "Deletes a regular user(not staff) after X days" - - def handle(self, *args, **options): - User = get_user_model() - interval = date.today() - timedelta( - days=settings.DELETE_USER_AFTER_X_DAYS_INACTIVE - ) - users_to_be_deleted = User.objects.filter( - is_active=False, - is_staff=False, - deactivated_on__lte=interval, - ) - - if users_to_be_deleted: - logger.info(f"\nDeleting users from before {interval}.") - results = users_to_be_deleted.delete() - num_of_deleted_users = results[1].get("accounts.User") - logger.info(f"\n{num_of_deleted_users} users were successfully deleted.") - else: - logger.info(f"\nNo users were deleted from before {interval}.") diff --git a/src/open_inwoner/accounts/middleware.py b/src/open_inwoner/accounts/middleware.py index a0ba9f61b3..24ce918f9b 100644 --- a/src/open_inwoner/accounts/middleware.py +++ b/src/open_inwoner/accounts/middleware.py @@ -29,15 +29,21 @@ def __call__(self, request): # If the user is currently not editing their information, but it is required # redirect to that view. - # DigiD can be disabled, in which case the digid app isn't available - digid_logout = "/digid/logout/" try: digid_logout = reverse("digid:logout") - except: # nosec - pass + digid_slo_redirect = reverse("digid:slo-redirect") + except NoReverseMatch: + # temporary fix to make tests pass in case reverse fails + digid_logout = "/digid/logout/" + digid_slo_redirect = "/digid/slo/redirect/" if ( not request.path.startswith( - (necessary_fields_url, reverse("logout"), digid_logout) + ( + necessary_fields_url, + reverse("logout"), + digid_logout, + digid_slo_redirect, + ) ) and request.user.require_necessary_fields() ): diff --git a/src/open_inwoner/accounts/migrations/0061_auto_20230612_1428.py b/src/open_inwoner/accounts/migrations/0061_auto_20230612_1428.py new file mode 100644 index 0000000000..bdb1328d79 --- /dev/null +++ b/src/open_inwoner/accounts/migrations/0061_auto_20230612_1428.py @@ -0,0 +1,100 @@ +# Generated by Django 3.2.15 on 2023-06-12 12:28 + +from django.db import migrations, models +import open_inwoner.utils.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0060_user_infix"), + ] + + operations = [ + migrations.AlterField( + model_name="invite", + name="invitee_first_name", + field=models.CharField( + help_text="The first name of the invitee.", + max_length=250, + validators=[open_inwoner.utils.validators.CharFieldValidator()], + verbose_name="First name", + ), + ), + migrations.AlterField( + model_name="invite", + name="invitee_last_name", + field=models.CharField( + help_text="The last name of the invitee", + max_length=250, + validators=[open_inwoner.utils.validators.CharFieldValidator()], + verbose_name="Last name", + ), + ), + migrations.AlterField( + model_name="user", + name="city", + field=models.CharField( + blank=True, + default="", + max_length=250, + validators=[open_inwoner.utils.validators.CharFieldValidator()], + verbose_name="City", + ), + ), + migrations.AlterField( + model_name="user", + name="display_name", + field=models.CharField( + blank=True, + default="", + max_length=255, + validators=[open_inwoner.utils.validators.CharFieldValidator()], + verbose_name="Display name", + ), + ), + migrations.AlterField( + model_name="user", + name="first_name", + field=models.CharField( + blank=True, + default="", + max_length=255, + validators=[open_inwoner.utils.validators.CharFieldValidator()], + verbose_name="First name", + ), + ), + migrations.AlterField( + model_name="user", + name="infix", + field=models.CharField( + blank=True, + default="", + max_length=64, + validators=[open_inwoner.utils.validators.CharFieldValidator()], + verbose_name="Infix", + ), + ), + migrations.AlterField( + model_name="user", + name="last_name", + field=models.CharField( + blank=True, + default="", + max_length=255, + validators=[open_inwoner.utils.validators.CharFieldValidator()], + verbose_name="Last name", + ), + ), + migrations.AlterField( + model_name="user", + name="street", + field=models.CharField( + blank=True, + default="", + max_length=250, + validators=[open_inwoner.utils.validators.CharFieldValidator()], + verbose_name="Street", + ), + ), + ] diff --git a/src/open_inwoner/accounts/migrations/0062_alter_user_deactivated_on.py b/src/open_inwoner/accounts/migrations/0062_alter_user_deactivated_on.py new file mode 100644 index 0000000000..39ea9c1c51 --- /dev/null +++ b/src/open_inwoner/accounts/migrations/0062_alter_user_deactivated_on.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.15 on 2023-06-23 08:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0061_auto_20230612_1428"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="deactivated_on", + field=models.DateField( + blank=True, + help_text="This is the date the user decided to deactivate their account. This field is deprecated since user profiles are now immediately deleted.", + null=True, + verbose_name="Deactivated on", + ), + ), + ] diff --git a/src/open_inwoner/accounts/models.py b/src/open_inwoner/accounts/models.py index 67040f7725..342fe0aa8e 100644 --- a/src/open_inwoner/accounts/models.py +++ b/src/open_inwoner/accounts/models.py @@ -18,10 +18,7 @@ from timeline_logger.models import TimelineLog from open_inwoner.utils.hash import create_sha256_hash -from open_inwoner.utils.validators import ( - validate_charfield_entry, - validate_phone_number, -) +from open_inwoner.utils.validators import CharFieldValidator, validate_phone_number from ..plans.models import PlanContact from .choices import ContactTypeChoices, LoginTypeChoices, StatusChoices, TypeChoices @@ -52,28 +49,28 @@ class User(AbstractBaseUser, PermissionsMixin): max_length=255, blank=True, default="", - validators=[validate_charfield_entry], + validators=[CharFieldValidator()], ) infix = models.CharField( verbose_name=_("Infix"), max_length=64, blank=True, default="", - validators=[validate_charfield_entry], + validators=[CharFieldValidator()], ) last_name = models.CharField( verbose_name=_("Last name"), max_length=255, blank=True, default="", - validators=[validate_charfield_entry], + validators=[CharFieldValidator()], ) display_name = models.CharField( verbose_name=_("Display name"), max_length=255, blank=True, default="", - validators=[validate_charfield_entry], + validators=[CharFieldValidator()], ) email = models.EmailField(verbose_name=_("Email address"), unique=True) phonenumber = models.CharField( @@ -124,7 +121,11 @@ class User(AbstractBaseUser, PermissionsMixin): ) birthday = models.DateField(verbose_name=_("Birthday"), null=True, blank=True) street = models.CharField( - verbose_name=_("Street"), default="", blank=True, max_length=250 + verbose_name=_("Street"), + default="", + blank=True, + max_length=250, + validators=[CharFieldValidator()], ) housenumber = models.CharField( verbose_name=_("House number"), default="", blank=True, max_length=250 @@ -133,13 +134,21 @@ class User(AbstractBaseUser, PermissionsMixin): verbose_name=_("Postcode"), null=True, blank=True, max_length=250 ) city = models.CharField( - verbose_name=_("City"), default="", blank=True, max_length=250 + verbose_name=_("City"), + default="", + blank=True, + max_length=250, + validators=[CharFieldValidator()], ) deactivated_on = models.DateField( verbose_name=_("Deactivated on"), null=True, blank=True, - help_text=_("This is the date the user decided to deactivate their account."), + help_text=_( + "This is the date the user decided to deactivate their account. " + "This field is deprecated since user profiles are now immediately " + "deleted." + ), ) is_prepopulated = models.BooleanField( verbose_name=_("Prepopulated"), @@ -245,11 +254,6 @@ def get_address(self): return f"{self.street} {self.housenumber}, {self.city}" return "" - def deactivate(self): - self.is_active = False - self.deactivated_on = date.today() - self.save() - def get_new_messages_total(self) -> int: return self.received_messages.filter(seen=False).count() @@ -661,13 +665,13 @@ class Invite(models.Model): verbose_name=_("First name"), max_length=250, help_text=_("The first name of the invitee."), - validators=[validate_charfield_entry], + validators=[CharFieldValidator()], ) invitee_last_name = models.CharField( verbose_name=_("Last name"), max_length=250, help_text=_("The last name of the invitee"), - validators=[validate_charfield_entry], + validators=[CharFieldValidator()], ) invitee_email = models.EmailField( verbose_name=_("Invitee email"), diff --git a/src/open_inwoner/accounts/signals.py b/src/open_inwoner/accounts/signals.py index 0f9cb96f10..2bd7693802 100644 --- a/src/open_inwoner/accounts/signals.py +++ b/src/open_inwoner/accounts/signals.py @@ -8,6 +8,8 @@ from open_inwoner.haalcentraal.utils import update_brp_data_in_db from open_inwoner.utils.logentry import user_action +from ..openklant.models import OpenKlantConfig +from ..openklant.services import update_user_from_klant from .choices import LoginTypeChoices MESSAGE_TYPE = { @@ -39,10 +41,15 @@ def log_user_login(sender, user, request, *args, **kwargs): # update brp fields when login with digid and brp is configured brp_config = HaalCentraalConfig.get_solo() - brp_version = settings.BRP_VERSION - if user.login_type == LoginTypeChoices.digid and brp_config.service: - update_brp_data_in_db(user, brp_version, initial=False) + oc_config = OpenKlantConfig.get_solo() + + if user.login_type == LoginTypeChoices.digid: + if brp_config.service: + update_brp_data_in_db(user, settings.BRP_VERSION, initial=False) + + if oc_config.klanten_service: + update_user_from_klant(user) @receiver(user_logged_out) diff --git a/src/open_inwoner/accounts/tests/test_auth.py b/src/open_inwoner/accounts/tests/test_auth.py index f632af7d3d..ed5b5ea3f9 100644 --- a/src/open_inwoner/accounts/tests/test_auth.py +++ b/src/open_inwoner/accounts/tests/test_auth.py @@ -73,12 +73,12 @@ def test_registration_fails_without_filling_out_last_name(self): self.assertEqual(user_query.count(), 0) def test_registration_fails_with_invalid_first_name_characters(self): - invalid_characters = "/\"\\,.:;'" + invalid_characters = '<>#/"\\,.:;' + register_page = self.app.get(reverse("django_registration_register")) + form = register_page.forms["registration-form"] for char in invalid_characters: with self.subTest(char=char): - register_page = self.app.get(reverse("django_registration_register")) - form = register_page.forms["registration-form"] form["email"] = self.user.email form["first_name"] = char form["last_name"] = self.user.last_name @@ -87,20 +87,21 @@ def test_registration_fails_with_invalid_first_name_characters(self): response = form.submit() expected_errors = { "first_name": [ - _("Uw invoer bevat een ongeldig teken: {char}").format( - char=char + _( + "Please make sure your input contains only valid characters " + "(letters, numbers, apostrophe, dash, space)." ) ] } self.assertEqual(response.context["form"].errors, expected_errors) def test_registration_fails_with_invalid_last_name_characters(self): - invalid_characters = "/\"\\,.:;'" + invalid_characters = '<>#/"\\,.:;' + register_page = self.app.get(reverse("django_registration_register")) + form = register_page.forms["registration-form"] for char in invalid_characters: with self.subTest(char=char): - register_page = self.app.get(reverse("django_registration_register")) - form = register_page.forms["registration-form"] form["email"] = self.user.email form["first_name"] = self.user.first_name form["last_name"] = char @@ -109,8 +110,68 @@ def test_registration_fails_with_invalid_last_name_characters(self): response = form.submit() expected_errors = { "last_name": [ - _("Uw invoer bevat een ongeldig teken: {char}").format( - char=char + _( + "Please make sure your input contains only valid characters " + "(letters, numbers, apostrophe, dash, space)." + ) + ] + } + self.assertEqual(response.context["form"].errors, expected_errors) + + def test_registration_fails_uniform_password(self): + passwords = [ + "lowercase123", + "UPPERCASE123", + "NODIGITS", + "nodigits", + "NoDigits", + "1238327879", + ] + register_page = self.app.get(reverse("django_registration_register")) + form = register_page.forms["registration-form"] + + for password in passwords: + with self.subTest(password=password): + form["email"] = self.user.email + form["first_name"] = self.user.first_name + form["last_name"] = self.user.last_name + form["password1"] = password + form["password2"] = password + response = form.submit() + expected_errors = { + "password2": [ + _( + "Your password must contain at least 1 upper-case letter, " + "1 lower-case letter, 1 digit." + ), + ] + } + self.assertEqual(response.context["form"].errors, expected_errors) + + def test_registration_fails_with_non_diverse_password(self): + passwords = [ + "pass_word-123", + "PASS_WORD-123", + "NoDigits", + "UPPERCASE123", + "lowercase123", + ] + register_page = self.app.get(reverse("django_registration_register")) + form = register_page.forms["registration-form"] + + for password in passwords: + with self.subTest(password=password): + form["email"] = self.user.email + form["first_name"] = self.user.first_name + form["last_name"] = self.user.last_name + form["password1"] = password + form["password2"] = password + response = form.submit() + expected_errors = { + "password2": [ + _( + "Your password must contain at least 1 upper-case letter, " + "1 lower-case letter, 1 digit." ) ] } @@ -139,8 +200,8 @@ def test_registration_inactive_user(self): form["email"] = inactive_user.email form["first_name"] = "John" form["last_name"] = "Smith" - form["password1"] = "somepassword" - form["password2"] = "somepassword" + form["password1"] = "SomePassword123" + form["password2"] = "SomePassword123" response = form.submit() @@ -168,8 +229,8 @@ def test_registration_with_invite(self): self.assertEqual(form["first_name"].value, contact.first_name) self.assertEqual(form["last_name"].value, contact.last_name) - form["password1"] = "somepassword" - form["password2"] = "somepassword" + form["password1"] = "SomePassword123" + form["password2"] = "SomePassword123" response = form.submit() @@ -210,8 +271,8 @@ def test_invite_url_not_in_session_after_successful_registration(self): register_page = self.app.get(f"{self.url}?invite={invite.key}") form = register_page.forms["registration-form"] - form["password1"] = "somepassword" - form["password2"] = "somepassword" + form["password1"] = "SomePassword123" + form["password2"] = "SomePassword123" response = form.submit() @@ -235,8 +296,8 @@ def test_registration_non_unique_email_different_case(self): form["email"] = "John@smith.com" form["first_name"] = "John" form["last_name"] = "Smith" - form["password1"] = "somepassword" - form["password2"] = "somepassword" + form["password1"] = "SomePassword123" + form["password2"] = "SomePassword123" response = form.submit() @@ -813,7 +874,7 @@ def test_submit_invalid_first_name_chars_fails(self): last_name="", login_type=LoginTypeChoices.digid, ) - invalid_characters = "/\"\\,.:;'" + invalid_characters = '<>#/"\\,.:;' for char in invalid_characters: with self.subTest(char=char): @@ -825,8 +886,9 @@ def test_submit_invalid_first_name_chars_fails(self): response = form.submit() expected_errors = { "first_name": [ - _("Uw invoer bevat een ongeldig teken: {char}").format( - char=char + _( + "Please make sure your input contains only valid characters " + "(letters, numbers, apostrophe, dash, space)." ) ] } @@ -839,7 +901,7 @@ def test_submit_invalid_last_name_chars_fails(self): last_name="", login_type=LoginTypeChoices.digid, ) - invalid_characters = "/\"\\,.:;'" + invalid_characters = '<>#/"\\,.:;' for char in invalid_characters: with self.subTest(char=char): @@ -851,8 +913,9 @@ def test_submit_invalid_last_name_chars_fails(self): response = form.submit() expected_errors = { "last_name": [ - _("Uw invoer bevat een ongeldig teken: {char}").format( - char=char + _( + "Please make sure your input contains only valid characters " + "(letters, numbers, apostrophe, dash, space)." ) ] } diff --git a/src/open_inwoner/accounts/tests/test_contact_views.py b/src/open_inwoner/accounts/tests/test_contact_views.py index 7d39a46d3c..55404a89cf 100644 --- a/src/open_inwoner/accounts/tests/test_contact_views.py +++ b/src/open_inwoner/accounts/tests/test_contact_views.py @@ -6,8 +6,8 @@ from django.urls import reverse from django.utils.translation import ugettext_lazy as _ +from cms import api from django_webtest import WebTest -from PIL import Image from open_inwoner.accounts.models import User from open_inwoner.utils.tests.helpers import create_image_bytes @@ -85,10 +85,40 @@ def test_contact_filter_without_any_contacts(self): ), ) - def test_contact_list_show_link_to_messages(self): - message_link = reverse("inbox:index", kwargs={"uuid": self.contact.uuid}) + def test_messages_enabled_disabled(self): + """Assert that `Stuur Bericht` is displayed if and only if the message page is published""" + + # case 1: no message page response = self.app.get(self.list_url, user=self.user) - self.assertContains(response, message_link) + + # self.assertNotIn(_("Stuur bericht"), response) + self.assertNotContains(response, _("Stuur bericht")) + + # case 2: unpublished message page + page = api.create_page( + "Mijn Berichten", + "cms/fullwidth.html", + "nl", + slug="berichten", + ) + page.application_namespace = "inbox" + page.save() + + response = self.app.get(self.list_url, user=self.user) + + self.assertNotContains(response, _("Stuur bericht")) + + # case 3: published message page + page.publish("nl") + page.save() + + response = self.app.get(self.list_url, user=self.user) + + icons = response.pyquery(".material-icons-outlined") + message_icon = next((icon for icon in icons if icon.text == "message"), None) + message_button_text = message_icon.tail.strip() + + self.assertEqual(_("Stuur bericht"), message_button_text) def test_contact_list_show_reversed(self): other_contact = UserFactory(first_name="reverse_contact_user_should_be_found") @@ -208,7 +238,7 @@ def test_adding_inactive_contact_fails(self): self.assertEqual(response.context["form"].errors, expected_errors) def test_adding_contact_with_invalid_first_name_chars_fails(self): - invalid_characters = "/\"\\,.:;'" + invalid_characters = '<>#/"\\,.:;' for char in invalid_characters: with self.subTest(char=char): @@ -220,15 +250,16 @@ def test_adding_contact_with_invalid_first_name_chars_fails(self): response = form.submit() expected_errors = { "first_name": [ - _("Uw invoer bevat een ongeldig teken: {char}").format( - char=char + _( + "Please make sure your input contains only valid characters " + "(letters, numbers, apostrophe, dash, space)." ) ] } self.assertEqual(response.context["form"].errors, expected_errors) def test_adding_contact_with_invalid_last_name_chars_fails(self): - invalid_characters = "/\"\\,.:;'" + invalid_characters = '<>#/"\\,.:;' for char in invalid_characters: with self.subTest(char=char): @@ -240,8 +271,9 @@ def test_adding_contact_with_invalid_last_name_chars_fails(self): response = form.submit() expected_errors = { "last_name": [ - _("Uw invoer bevat een ongeldig teken: {char}").format( - char=char + _( + "Please make sure your input contains only valid characters " + "(letters, numbers, apostrophe, dash, space)." ) ] } @@ -366,19 +398,12 @@ def test_accepted_contact_appears_in_both_contact_lists(self): self.assertContains(response, self.user.first_name) self.assertContains(response, self.user.last_name) - self.assertContains( - response, reverse("inbox:index", kwargs={"uuid": self.user.uuid}) - ) # Sender contact list page response = self.app.get(self.list_url, user=self.user) self.assertContains(response, existing_user.first_name) self.assertContains(response, existing_user.last_name) - self.assertContains( - response, - reverse("inbox:index", kwargs={"uuid": existing_user.uuid}), - ) def test_post_with_no_params_in_contact_approval_returns_bad_request(self): existing_user = UserFactory(email="ex@example.com") diff --git a/src/open_inwoner/accounts/tests/test_deleteinactiveusers.py b/src/open_inwoner/accounts/tests/test_deleteinactiveusers.py deleted file mode 100644 index 5fb1215d1a..0000000000 --- a/src/open_inwoner/accounts/tests/test_deleteinactiveusers.py +++ /dev/null @@ -1,92 +0,0 @@ -from django.core.management import call_command -from django.test import TestCase - -from freezegun import freeze_time - -from ..models import User -from .factories import UserFactory - - -class TestCommand(TestCase): - @freeze_time("2021-10-01") - def test_command_deletes_inactive_regular_user_when_X_days_have_passed( - self, - ): - user = UserFactory(deactivated_on="2021-09-17", is_active=False, is_staff=False) - - with self.assertLogs() as captured: - call_command("deleteinactiveusers") - - self.assertEqual( - captured.records[1].getMessage(), "\n1 users were successfully deleted." - ) - self.assertFalse(User.objects.filter(id=user.id).exists()) - - @freeze_time("2021-10-01") - def test_command_does_not_delete_active_staff_user_when_X_days_have_passed( - self, - ): - user = UserFactory(deactivated_on="2021-09-17", is_active=True, is_staff=True) - - with self.assertLogs() as captured: - call_command("deleteinactiveusers") - - self.assertEqual( - captured.records[0].getMessage(), - "\nNo users were deleted from before 2021-09-17.", - ) - self.assertTrue(User.objects.filter(id=user.id).exists()) - - @freeze_time("2021-10-01") - def test_command_does_not_delete_active_staff_user_when_X_days_have_not_passed( - self, - ): - user = UserFactory(deactivated_on="2021-09-25", is_active=True, is_staff=True) - call_command("deleteinactiveusers") - - self.assertTrue(User.objects.filter(id=user.id).exists()) - - @freeze_time("2021-10-01") - def test_command_does_not_delete_active_regular_user_when_X_days_have_not_passed( - self, - ): - user = UserFactory(deactivated_on="2021-09-25", is_active=True, is_staff=False) - call_command("deleteinactiveusers") - - self.assertTrue(User.objects.filter(id=user.id).exists()) - - @freeze_time("2021-10-01") - def test_command_does_not_delete_inactive_regular_user_when_X_days_have_not_passed( - self, - ): - user = UserFactory(deactivated_on="2021-09-25", is_active=False, is_staff=False) - call_command("deleteinactiveusers") - - self.assertTrue(User.objects.filter(id=user.id).exists()) - - @freeze_time("2021-10-01") - def test_command_does_not_delete_inactive_staff_user_when_X_days_have_passed( - self, - ): - user = UserFactory(deactivated_on="2021-09-17", is_active=False, is_staff=True) - call_command("deleteinactiveusers") - - self.assertTrue(User.objects.filter(id=user.id).exists()) - - @freeze_time("2021-10-01") - def test_command_does_not_delete_active_regular_user_when_X_days_have_passed( - self, - ): - user = UserFactory(deactivated_on="2021-09-17", is_active=True, is_staff=False) - call_command("deleteinactiveusers") - - self.assertTrue(User.objects.filter(id=user.id).exists()) - - @freeze_time("2021-10-01") - def test_command_does_not_delete_inactive_staff_user_when_X_days_have_not_passed( - self, - ): - user = UserFactory(deactivated_on="2021-09-25", is_active=False, is_staff=True) - call_command("deleteinactiveusers") - - self.assertTrue(User.objects.filter(id=user.id).exists()) diff --git a/src/open_inwoner/accounts/tests/test_logging.py b/src/open_inwoner/accounts/tests/test_logging.py index 29db1ab98b..a2fae8f1c6 100644 --- a/src/open_inwoner/accounts/tests/test_logging.py +++ b/src/open_inwoner/accounts/tests/test_logging.py @@ -205,9 +205,9 @@ def test_logout_is_logged(self): }, ) - def test_users_deactivation_is_logged(self): + def test_users_deletion_is_logged(self): form = self.app.get(reverse("profile:detail"), user=self.user).forms[ - "deactivate-form" + "delete-form" ] form.submit() log_entry = TimelineLog.objects.last() @@ -215,11 +215,10 @@ def test_users_deactivation_is_logged(self): self.assertEqual( log_entry.timestamp.strftime("%m/%d/%Y, %H:%M:%S"), "10/18/2021, 13:00:00" ) - self.assertEqual(log_entry.content_object.id, self.user.id) self.assertEqual( log_entry.extra_data, { - "message": _("user was deactivated via frontend"), + "message": _("user was deleted via frontend"), "action_flag": list(LOG_ACTIONS[4]), "content_object_repr": self.user.email, }, diff --git a/src/open_inwoner/accounts/tests/test_profile_views.py b/src/open_inwoner/accounts/tests/test_profile_views.py index c13f662501..3962c9a8f2 100644 --- a/src/open_inwoner/accounts/tests/test_profile_views.py +++ b/src/open_inwoner/accounts/tests/test_profile_views.py @@ -1,12 +1,10 @@ -import io - from django.test import override_settings from django.urls import reverse from django.utils.translation import ugettext_lazy as _ import requests_mock +from cms import api from django_webtest import WebTest -from PIL import Image from timeline_logger.models import TimelineLog from webtest import Upload @@ -14,15 +12,18 @@ from open_inwoner.cms.profile.cms_appconfig import ProfileConfig from open_inwoner.haalcentraal.tests.mixins import HaalCentraalMixin from open_inwoner.pdc.tests.factories import CategoryFactory +from open_inwoner.plans.tests.factories import PlanFactory from open_inwoner.utils.logentry import LOG_ACTIONS -from open_inwoner.utils.tests.helpers import create_image_bytes +from open_inwoner.utils.tests.helpers import AssertTimelineLogMixin, create_image_bytes from ...cms.profile.cms_apps import ProfileApphook from ...cms.tests import cms_tools +from ...openklant.tests.data import MockAPIReadPatchData from ...questionnaire.tests.factories import QuestionnaireStepFactory from ..choices import ContactTypeChoices, LoginTypeChoices from ..forms import BrpUserForm, UserForm -from .factories import ActionFactory, DocumentFactory, UserFactory +from ..models import User +from .factories import ActionFactory, DigidUserFactory, DocumentFactory, UserFactory @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") @@ -93,49 +94,6 @@ def test_only_open_actions(self): self.assertEquals(response.status_code, 200) self.assertContains(response, "0 acties staan open.") - def test_deactivate_account(self): - response = self.app.get(self.url, user=self.user) - self.assertEquals(response.status_code, 200) - form = response.forms["deactivate-form"] - base_response = form.submit() - self.assertEquals(base_response.url, self.return_url) - followed_response = base_response.follow().follow() - self.assertEquals(followed_response.status_code, 200) - self.user.refresh_from_db() - self.assertFalse(self.user.is_active) - self.assertIsNotNone(self.user.deactivated_on) - - def test_deactivate_account_staff(self): - self.user.is_staff = True - self.user.save() - response = self.app.get(self.url, user=self.user) - self.assertEquals(response.status_code, 200) - form = response.forms["deactivate-form"] - base_response = form.submit() - self.assertEquals(base_response.url, self.url) - followed_response = base_response.follow() - self.assertEquals(followed_response.status_code, 200) - self.user.refresh_from_db() - self.assertTrue(self.user.is_active) - self.assertIsNone(self.user.deactivated_on) - - def test_deactivate_account_digid(self): - """ - check that user is redirected to digid:logout - """ - user = UserFactory.create( - login_type=LoginTypeChoices.digid, email="john@smith.nl" - ) - - response = self.app.get(self.url, user=user) - self.assertEquals(response.status_code, 200) - form = response.forms["deactivate-form"] - - response = form.submit() - - self.assertEquals(response.status_code, 302) - self.assertEquals(response.url, reverse("digid:logout")) - def test_get_documents_sorted(self): """ check that the new document is shown first @@ -191,9 +149,45 @@ def test_expected_message_is_shown_when_all_notifications_disabled(self): response = self.app.get(self.url, user=self.user) self.assertContains(response, _("You do not have any notifications enabled.")) + def test_messages_enabled_disabled(self): + """Assert that `Stuur een bericht` is displayed if and only if the message page is published""" + + begeleider = UserFactory(contact_type=ContactTypeChoices.begeleider) + self.user.user_contacts.add(begeleider) + + # case 1: no message page + response = self.app.get(self.url, user=self.user) + + self.assertNotContains(response, _("Stuur een bericht")) + + # case 2: unpublished message page + page = api.create_page( + "Mijn Berichten", + "cms/fullwidth.html", + "nl", + slug="berichten", + ) + page.application_namespace = "inbox" + page.save() + + response = self.app.get(self.url, user=self.user) + + self.assertNotContains(response, _("Stuur een bericht")) + + # case 3: published message page + page.publish("nl") + page.save() + + response = self.app.get(self.url, user=self.user) + + message_link = response.pyquery("[title='Stuur een bericht']") + link_text = message_link.find(".link__text").text + + self.assertEqual(link_text(), _("Stuur een bericht")) + @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") -class EditProfileTests(WebTest): +class EditProfileTests(AssertTimelineLogMixin, WebTest): def setUp(self): self.url = reverse("profile:edit") self.return_url = reverse("profile:detail") @@ -265,55 +259,33 @@ def test_save_filled_form(self): self.assertEquals(self.user.postcode, "1013 RM") self.assertEquals(self.user.city, "Amsterdam") - def test_save_with_invalid_first_name_chars_fails(self): - invalid_characters = "/\"\\,.:;'" + def test_name_validation(self): + invalid_characters = '<>#/"\\,.:;' for char in invalid_characters: with self.subTest(char=char): response = self.app.get(self.url, user=self.user, status=200) form = response.forms["profile-edit"] - form["first_name"] = char - form["last_name"] = "Last name" - form["display_name"] = "a nickname" - form["phonenumber"] = "06987878787" - form["birthday"] = "21-01-1992" - form["street"] = "Keizersgracht" - form["housenumber"] = "17 d" - form["postcode"] = "1013 RM" - form["city"] = "Amsterdam" - response = form.submit() - expected_errors = { - "first_name": [ - _("Uw invoer bevat een ongeldig teken: {char}").format( - char=char - ) - ] - } - self.assertEqual(response.context["form"].errors, expected_errors) - - def test_save_with_invalid_last_name_chars_fails(self): - invalid_characters = "/\"\\,.:;'" + form["first_name"] = "test" + char + form["infix"] = char + "test" + form["last_name"] = "te" + char + "st" + form["display_name"] = "te" + char + "st" + form["city"] = "te" + char + "st" + form["street"] = "te" + char + "st" - for char in invalid_characters: - with self.subTest(char=char): - response = self.app.get(self.url, user=self.user, status=200) - form = response.forms["profile-edit"] - form["first_name"] = "John" - form["last_name"] = char - form["display_name"] = "a nickname" - form["phonenumber"] = "06987878787" - form["birthday"] = "21-01-1992" - form["street"] = "Keizersgracht" - form["housenumber"] = "17 d" - form["postcode"] = "1013 RM" - form["city"] = "Amsterdam" response = form.submit() + + error_msg = _( + "Please make sure your input contains only valid characters " + "(letters, numbers, apostrophe, dash, space)." + ) expected_errors = { - "last_name": [ - _("Uw invoer bevat een ongeldig teken: {char}").format( - char=char - ) - ] + "first_name": [error_msg], + "infix": [error_msg], + "last_name": [error_msg], + "display_name": [error_msg], + "city": [error_msg], + "street": [error_msg], } self.assertEqual(response.context["form"].errors, expected_errors) @@ -435,6 +407,203 @@ def test_image_field_is_not_rendered_when_begeleider_and_digid_login(self): self.assertNotIn("image", form.fields.keys()) self.assertEqual(response.pyquery("#id_image"), []) + @requests_mock.Mocker() + def test_modify_phone_and_email_updates_klant_api(self, m): + MockAPIReadPatchData.setUpServices() + data = MockAPIReadPatchData().install_mocks(m) + + response = self.app.get(self.url, user=data.user) + + # reset noise from signals + m.reset_mock() + self.resetTimelineLogs() + + form = response.forms["profile-edit"] + form["email"] = "new@example.com" + form["phonenumber"] = "01234456789" + form.submit() + + # user data tested in other cases + + self.assertTrue(data.matchers[0].called) + klant_patch_data = data.matchers[1].request_history[0].json() + self.assertEqual( + klant_patch_data, + { + "emailadres": "new@example.com", + "telefoonnummer": "01234456789", + }, + ) + self.assertTimelineLog("retrieved klant for BSN-user") + self.assertTimelineLog( + "patched klant from user profile edit with fields: emailadres, telefoonnummer" + ) + + @requests_mock.Mocker() + def test_modify_phone_updates_klant_api_but_skips_unchanged(self, m): + MockAPIReadPatchData.setUpServices() + data = MockAPIReadPatchData().install_mocks(m) + + response = self.app.get(self.url, user=data.user) + + # reset noise from signals + m.reset_mock() + self.resetTimelineLogs() + + form = response.forms["profile-edit"] + form.submit() + + # user data tested in other cases + + self.assertFalse(data.matchers[0].called) + self.assertFalse(data.matchers[1].called) + + @requests_mock.Mocker() + def test_modify_phone_updates_klant_api_but_skip_unchanged_email(self, m): + MockAPIReadPatchData.setUpServices() + data = MockAPIReadPatchData().install_mocks(m) + + response = self.app.get(self.url, user=data.user) + + # reset noise from signals + m.reset_mock() + self.resetTimelineLogs() + + form = response.forms["profile-edit"] + form["phonenumber"] = "01234456789" + form.submit() + + # user data tested in other cases + + self.assertTrue(data.matchers[0].called) + klant_patch_data = data.matchers[1].request_history[0].json() + self.assertEqual( + klant_patch_data, + { + "telefoonnummer": "01234456789", + }, + ) + self.assertTimelineLog("retrieved klant for BSN-user") + self.assertTimelineLog( + "patched klant from user profile edit with fields: telefoonnummer" + ) + + @requests_mock.Mocker() + def test_modify_phone_updates_klant_api_but_skip_unchanged_phone(self, m): + MockAPIReadPatchData.setUpServices() + data = MockAPIReadPatchData().install_mocks(m) + + response = self.app.get(self.url, user=data.user) + + # reset noise from signals + m.reset_mock() + self.resetTimelineLogs() + + form = response.forms["profile-edit"] + form["email"] = "new@example.com" + form.submit() + + # user data tested in other cases + + self.assertTrue(data.matchers[0].called) + klant_patch_data = data.matchers[1].request_history[0].json() + self.assertEqual( + klant_patch_data, + { + "emailadres": "new@example.com", + }, + ) + self.assertTimelineLog("retrieved klant for BSN-user") + self.assertTimelineLog( + "patched klant from user profile edit with fields: emailadres" + ) + + +@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") +class ProfileDeleteTest(WebTest): + csrf_checks = False + + @classmethod + def setUpTestData(cls): + cls.url = reverse("profile:detail") + + def test_delete_regular_user_success(self): + user = UserFactory() + + # get profile page + response = self.app.get(self.url, user=user) + + # check delete + response = response.forms["delete-form"].submit() + self.assertIsNone(User.objects.first()) + + # check redirect + self.assertRedirects( + self.app.get(response.url), + reverse("pages-root"), + status_code=302, + target_status_code=200, + fetch_redirect_response=True, + ) + + def test_delete_user_with_digid_login_success(self): + user = DigidUserFactory() + + # get profile page + response = self.app.get(self.url, user=user) + + # check user deleted + response = response.forms["delete-form"].submit() + self.assertIsNone(User.objects.first()) + + # check redirect + self.assertRedirects( + self.app.get(response.url), + reverse("pages-root"), + status_code=302, + target_status_code=200, + fetch_redirect_response=True, + ) + + def test_delete_regular_user_as_plan_contact_fail(self): + user = UserFactory() + PlanFactory.create(plan_contacts=[user]) + + # get profile page + response = self.app.get(self.url, user=user) + + # check user not deleted + response = response.forms["delete-form"].submit() + self.assertEqual(User.objects.first(), user) + + # check redirect + self.assertRedirects( + response, + reverse("profile:detail"), + status_code=302, + target_status_code=200, + fetch_redirect_response=True, + ) + + def test_delete_staff_user_via_frontend_does_not_work(self): + user = UserFactory(is_staff=True) + + # get profile page + response = self.app.get(self.url, user=user) + + # check staff user not deleted + response = response.forms["delete-form"].submit() + self.assertEqual(User.objects.first(), user) + + # check redirect + self.assertRedirects( + response, + reverse("profile:detail"), + status_code=302, + target_status_code=200, + fetch_redirect_response=True, + ) + @requests_mock.Mocker() @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") diff --git a/src/open_inwoner/accounts/views/contacts.py b/src/open_inwoner/accounts/views/contacts.py index 1aefd684b7..2741879f1c 100644 --- a/src/open_inwoner/accounts/views/contacts.py +++ b/src/open_inwoner/accounts/views/contacts.py @@ -11,6 +11,7 @@ from mail_editor.helpers import find_template from view_breadcrumbs import BaseBreadcrumbMixin +from open_inwoner.cms.utils.page_display import inbox_page_is_published from open_inwoner.utils.views import CommonPageMixin, LogMixin from ..forms import ContactCreateForm, ContactFilterForm @@ -48,6 +49,7 @@ def get_context_data(self, **kwargs): context["contacts_for_approval"] = user.get_contacts_for_approval() context["pending_invitations"] = user.get_pending_invitations() context["form"] = ContactFilterForm(data=self.request.GET) + context["inbox_page_is_published"] = inbox_page_is_published() return context diff --git a/src/open_inwoner/accounts/views/profile.py b/src/open_inwoner/accounts/views/profile.py index 48e1ee6d50..d3752b9746 100644 --- a/src/open_inwoner/accounts/views/profile.py +++ b/src/open_inwoner/accounts/views/profile.py @@ -3,6 +3,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q from django.forms.forms import Form from django.http import HttpResponseRedirect from django.shortcuts import redirect @@ -20,11 +21,14 @@ LoginTypeChoices, StatusChoices, ) +from open_inwoner.cms.utils.page_display import inbox_page_is_published from open_inwoner.haalcentraal.utils import fetch_brp_data +from open_inwoner.plans.models import Plan from open_inwoner.questionnaire.models import QuestionnaireStep from open_inwoner.utils.mixins import ExportMixin from open_inwoner.utils.views import CommonPageMixin, LogMixin +from ...openklant.wrap import fetch_klant_for_bsn, patch_klant from ..forms import BrpUserForm, CategoriesForm, UserForm, UserNotificationsForm from ..models import Action, User @@ -52,8 +56,12 @@ def get_context_data(self, **kwargs): context["anchors"] = [ ("#title", _("Persoonlijke gegevens")), ("#overview", _("Persoonlijk overzicht")), - ("#files", _("Bestanden")), ] + + user_files = user.get_all_files() + if user_files: + context["anchors"].append(("#files", _("Bestanden"))) + # List of names of 'mentor' users that are a contact of me mentor_contacts = [ c.get_full_name() @@ -70,7 +78,7 @@ def get_context_data(self, **kwargs): .order_by("end_date") .first() ) - context["files"] = user.get_all_files() + context["files"] = user_files context["category_text"] = user.get_interests() context["action_text"] = _( f"{Action.objects.visible().connected(self.request.user).filter(status=StatusChoices.open).count()} acties staan open." @@ -93,18 +101,33 @@ def get_context_data(self, **kwargs): published=True ).exists() context["can_change_password"] = user.login_type != LoginTypeChoices.digid + context["inbox_page_is_published"] = inbox_page_is_published() return context def post(self, request, *args, **kwargs): if request.user.is_authenticated and not request.user.is_staff: instance = User.objects.get(id=request.user.id) - self.request.user.deactivate() - self.log_user_action(instance, _("user was deactivated via frontend")) - return redirect(instance.get_logout_url()) + # check if there are still plans created by or associated witht the user + if Plan.objects.connected(instance): + messages.warning( + request, + _( + "Your profile could not be deleted because you still " + "have plans associated with it." + ), + ) + return redirect("profile:detail") + + # continue with delete + self.log_user_action(instance, _("user was deleted via frontend")) + instance.delete() + request.session.flush() + + return redirect(reverse("logout")) else: - messages.warning(request, _("Uw account kon niet worden gedeactiveerd")) + messages.warning(request, _("Uw account kon niet worden verwijderd")) return redirect("profile:detail") @@ -129,9 +152,38 @@ 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}) + + 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 or user.login_type != LoginTypeChoices.digid: + return + field_mapping = { + "emailadres": "email", + "telefoonnummer": "phonenumber", + } + update_data = { + api_name: user_form_data[local_name] + for api_name, local_name in field_mapping.items() + if user_form_data.get(local_name) + } + if update_data: + klant = fetch_klant_for_bsn(user.bsn) + if klant: + self.log_system_action( + "retrieved klant for BSN-user", user=self.request.user + ) + klant = patch_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, + ) + def get_form_class(self): user = self.request.user if user.is_digid_and_brp(): @@ -164,7 +216,7 @@ def get_object(self): def form_valid(self, form): form.save() - + messages.success(self.request, _("Uw wijzigingen zijn opgeslagen")) self.log_change(self.object, _("categories were modified")) return HttpResponseRedirect(self.get_success_url()) @@ -259,7 +311,7 @@ def get_form_kwargs(self): def form_valid(self, form): form.save() - + messages.success(self.request, _("Uw wijzigingen zijn opgeslagen")) self.log_change(self.object, _("users notifications were modified")) return HttpResponseRedirect(self.get_success_url()) diff --git a/src/open_inwoner/cms/banner/cms_plugins.py b/src/open_inwoner/cms/banner/cms_plugins.py index cc6a9d602c..406aa25cf9 100644 --- a/src/open_inwoner/cms/banner/cms_plugins.py +++ b/src/open_inwoner/cms/banner/cms_plugins.py @@ -13,6 +13,7 @@ class BannerImagePlugin(CMSPluginBase): form = BannerImageForm name = _("Banner Image Plugin") render_template = "cms/banner/banner_image_plugin.html" + cache = False @plugin_pool.register_plugin @@ -21,3 +22,4 @@ class BannerTextPlugin(CMSPluginBase): form = BannerTextForm name = _("Banner Text Plugin") render_template = "cms/banner/banner_text_plugin.html" + cache = False diff --git a/src/open_inwoner/cms/banner/tests/__init__.py b/src/open_inwoner/cms/banner/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/open_inwoner/cms/banner/tests/test_plugin_banner.py b/src/open_inwoner/cms/banner/tests/test_plugin_banner.py new file mode 100644 index 0000000000..bcee63c78c --- /dev/null +++ b/src/open_inwoner/cms/banner/tests/test_plugin_banner.py @@ -0,0 +1,52 @@ +import os + +from django.test import TestCase + +from open_inwoner.cms.tests import cms_tools +from open_inwoner.utils.test import temp_media_root +from open_inwoner.utils.tests.factories import FilerImageFactory + +from ..cms_plugins import BannerImagePlugin, BannerTextPlugin + + +@temp_media_root() +class TestBannerImage(TestCase): + def test_banner_image_is_rendered_in_plugin(self): + image = FilerImageFactory() + html, context = cms_tools.render_plugin( + BannerImagePlugin, plugin_data={"image": image} + ) + self.assertIn(os.path.basename(image.file.name), html) + self.assertIn('