diff --git a/docs/install/dev.rst b/docs/install/dev.rst index d54a4f7370..38b05b6a72 100644 --- a/docs/install/dev.rst +++ b/docs/install/dev.rst @@ -32,12 +32,6 @@ Quick start Next steps ---------- -Optionally, you can load demo data and extract demo media files:: - - $ python src/manage.py loaddata demo - $ cd media - $ tar -xzf demo.tgz - You can now run your installation and point your browser to the address given by this command:: diff --git a/package-lock.json b/package-lock.json index 148f1e133a..7e89970f39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13905,18 +13905,18 @@ } }, "vm2": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.9.tgz", - "integrity": "sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw==", + "version": "3.9.11", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.11.tgz", + "integrity": "sha512-PFG8iJRSjvvBdisowQ7iVF580DXb1uCIiGaXgm7tynMR1uTBlv7UJlB1zdv5KJ+Tmq1f0Upnj3fayoEOPpCBKg==", "requires": { "acorn": "^8.7.0", "acorn-walk": "^8.2.0" }, "dependencies": { "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==" }, "acorn-walk": { "version": "8.2.0", diff --git a/requirements/base.in b/requirements/base.in index 6f6345e8a4..14f545e91c 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -44,6 +44,7 @@ django-rich django-csp django-csp-reports mozilla-django-oidc-db +django-open-forms-client # API libraries djangorestframework diff --git a/requirements/base.txt b/requirements/base.txt index c8e543799b..26a0a481ad 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -65,6 +65,7 @@ django==3.2.15 # django-hijack # django-import-export # django-localflavor + # django-open-forms-client # django-otp # django-phonenumber-field # django-polymorphic @@ -141,6 +142,8 @@ django-localflavor==3.1 # via -r requirements/base.in django-mptt==0.13.4 # via django-filer +django-open-forms-client==0.2.2 + # via -r requirements/base.in django-ordered-model==3.4.3 # via # -r requirements/base.in @@ -176,6 +179,7 @@ django-sniplates==0.7.0 django-solo==1.2.0 # via # -r requirements/base.in + # django-open-forms-client # mozilla-django-oidc-db # zgw-consumers django-timeline-logger==2.0.0 @@ -323,6 +327,7 @@ redis==3.5.3 # via django-redis requests==2.26.0 # via + # django-open-forms-client # django-rosetta # gemma-zds-client # mozilla-django-oidc diff --git a/requirements/ci.txt b/requirements/ci.txt index a1441b0aa4..88f3f48e39 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -108,6 +108,7 @@ django==3.2.15 # django-hijack # django-import-export # django-localflavor + # django-open-forms-client # django-otp # django-phonenumber-field # django-polymorphic @@ -233,6 +234,10 @@ django-mptt==0.13.4 # -c requirements/base.txt # -r requirements/base.txt # django-filer +django-open-forms-client==0.2.2 + # via + # -c requirements/base.txt + # -r requirements/base.txt django-ordered-model==3.4.3 # via # -c requirements/base.txt @@ -297,6 +302,7 @@ django-solo==1.2.0 # via # -c requirements/base.txt # -r requirements/base.txt + # django-open-forms-client # mozilla-django-oidc-db # zgw-consumers django-timeline-logger==2.0.0 @@ -614,6 +620,7 @@ requests==2.26.0 # via # -c requirements/base.txt # -r requirements/base.txt + # django-open-forms-client # django-rosetta # gemma-zds-client # mozilla-django-oidc diff --git a/requirements/dev.txt b/requirements/dev.txt index 8dfa45f199..24e842f7a3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -135,6 +135,7 @@ django==3.2.15 # django-hijack # django-import-export # django-localflavor + # django-open-forms-client # django-otp # django-phonenumber-field # django-polymorphic @@ -264,6 +265,10 @@ django-mptt==0.13.4 # -c requirements/ci.txt # -r requirements/ci.txt # django-filer +django-open-forms-client==0.2.2 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt django-ordered-model==3.4.3 # via # -c requirements/ci.txt @@ -328,6 +333,7 @@ django-solo==1.2.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # django-open-forms-client # mozilla-django-oidc-db # zgw-consumers django-timeline-logger==2.0.0 @@ -712,6 +718,7 @@ requests==2.26.0 # -c requirements/ci.txt # -r requirements/ci.txt # ddt-api-calls + # django-open-forms-client # django-rosetta # gemma-zds-client # mozilla-django-oidc diff --git a/src/open_inwoner/accounts/admin.py b/src/open_inwoner/accounts/admin.py index 1c7337e478..738df2f0fe 100644 --- a/src/open_inwoner/accounts/admin.py +++ b/src/open_inwoner/accounts/admin.py @@ -96,6 +96,13 @@ class ActionAdmin(UUIDAdminFirstInOrder, PrivateMediaMixin, admin.ModelAdmin): @admin.register(Contact) class ContactAdmin(UUIDAdminFirstInOrder, admin.ModelAdmin): readonly_fields = ("uuid",) + search_fields = ( + "first_name", + "last_name", + "email", + "contact_user__email", + "created_by__email", + ) list_display = ( "first_name", "last_name", diff --git a/src/open_inwoner/accounts/forms.py b/src/open_inwoner/accounts/forms.py index 2097aee16a..4cbc77c562 100644 --- a/src/open_inwoner/accounts/forms.py +++ b/src/open_inwoner/accounts/forms.py @@ -134,12 +134,16 @@ def clean(self): cleaned_data = super().clean() email = cleaned_data.get("email") - if self.create and email and self.user.contacts.filter(email=email).exists(): - raise ValidationError( - _( - "Het ingevoerde e-mailadres komt al voor in uw contactpersonen. Pas de gegevens aan en probeer het opnieuw." + if self.create and email: + if ( + self.user.contacts.filter(email=email).exists() + or self.user.assigned_contacts.filter(created_by__email=email).exists() + ): + raise ValidationError( + _( + "Het ingevoerde e-mailadres komt al voor in uw contactpersonen. Pas de gegevens aan en probeer het opnieuw." + ) ) - ) def save(self, commit=True): if not self.instance.pk: diff --git a/src/open_inwoner/accounts/query.py b/src/open_inwoner/accounts/query.py index 02ed0e30d1..8f18628a01 100644 --- a/src/open_inwoner/accounts/query.py +++ b/src/open_inwoner/accounts/query.py @@ -105,17 +105,15 @@ def get_extended_contacts_for_user(self, me): - other_user_email - other_user_phonenumber (Null in case of reversed contacts) - If the user and other user have contacts with each other only mine contact is shown + If the user and other user have contacts with each other return both contacts """ my_contacts_users = self.filter(created_by=me).values_list( "contact_user", flat=True ) return ( - self.filter( - Q(created_by=me) - | Q(~Q(created_by__in=my_contacts_users), contact_user=me) - ) + self.filter(Q(created_by=me) | Q(contact_user=me)) + .distinct() .annotate(reverse=Case(When(created_by=me, then=False), default=True)) .annotate( other_user_id=Case( @@ -135,6 +133,12 @@ def get_extended_contacts_for_user(self, me): default=F("created_by__last_name"), ) ) + .annotate( + other_user_type=Case( + When(created_by=me, then=F("contact_user__contact_type")), + default=F("created_by__contact_type"), + ) + ) .annotate( other_user_email=Case( When(created_by=me, then=F("contact_user__email")), diff --git a/src/open_inwoner/accounts/templates/accounts/inbox.html b/src/open_inwoner/accounts/templates/accounts/inbox.html index 37ffb3b8ac..81884a4c69 100644 --- a/src/open_inwoner/accounts/templates/accounts/inbox.html +++ b/src/open_inwoner/accounts/templates/accounts/inbox.html @@ -20,7 +20,7 @@

{% trans 'Mijn berichten' %}

{% render_list %} {% for conversation in conversations.page_obj %} - {% querystring conversation.other_user_email conversations.page_obj.number query='with={}&page={}' as href %} + {% querystring conversation.other_user_email|urlencode conversations.page_obj.number query='with={}&page={}' as href %} {% firstof '?'|add:href as href %} {% firstof conversation.other_user_first_name|add:' '|add:conversation.other_user_last_name as other_user_fullname %} diff --git a/src/open_inwoner/accounts/templates/registration/login.html b/src/open_inwoner/accounts/templates/registration/login.html index b2d7b1cfd4..8f621135d9 100644 --- a/src/open_inwoner/accounts/templates/registration/login.html +++ b/src/open_inwoner/accounts/templates/registration/login.html @@ -16,14 +16,15 @@

{% trans 'Welkom' %}

{% if login_text %}

{{ login_text }}

{% endif %}
- + {% if settings.DIGID_ENABLED %} {% render_card direction='horizontal' tinted=True %} {% link href='digid:login' text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} {% endrender_card %} - + {% endif %} + {% get_solo 'mozilla_django_oidc_db.OpenIDConnectConfig' as oidc_config %} {% get_solo 'configurations.SiteConfiguration' as site_config %} {% if oidc_config.enabled %} diff --git a/src/open_inwoner/accounts/tests/test_auth.py b/src/open_inwoner/accounts/tests/test_auth.py index f3a64a091c..8818d05d59 100644 --- a/src/open_inwoner/accounts/tests/test_auth.py +++ b/src/open_inwoner/accounts/tests/test_auth.py @@ -113,12 +113,7 @@ def test_registration_with_invite(self): self.assertEqual(invite.invitee, user) # reverse contact checks - self.assertEqual(user.contacts.count(), 1) - reverse_contact = user.contacts.get() - self.assertEqual(reverse_contact.contact_user, contact.created_by) - self.assertEqual(reverse_contact.email, contact.created_by.email) - self.assertEqual(reverse_contact.first_name, contact.created_by.first_name) - self.assertEqual(reverse_contact.last_name, contact.created_by.last_name) + self.assertEqual(user.contacts.count(), 0) def test_registration_active_user(self): """the user should be redirected to the registration complete page""" @@ -276,12 +271,7 @@ def test_submit_with_invite(self): self.assertEqual(invite.invitee, user) # reverse contact checks - self.assertEqual(user.contacts.count(), 1) - reverse_contact = user.contacts.get() - self.assertEqual(reverse_contact.contact_user, contact.created_by) - self.assertEqual(reverse_contact.email, contact.created_by.email) - self.assertEqual(reverse_contact.first_name, contact.created_by.first_name) - self.assertEqual(reverse_contact.last_name, contact.created_by.last_name) + self.assertEqual(user.contacts.count(), 0) def test_submit_not_unique_email(self): UserFactory.create(email="john@smith.com") diff --git a/src/open_inwoner/accounts/tests/test_contact_query.py b/src/open_inwoner/accounts/tests/test_contact_query.py index 679de236aa..5382c7373d 100644 --- a/src/open_inwoner/accounts/tests/test_contact_query.py +++ b/src/open_inwoner/accounts/tests/test_contact_query.py @@ -62,9 +62,9 @@ def test_mine_and_reverse_contact(self): contacts = Contact.objects.get_extended_contacts_for_user(self.me) - self.assertEqual(contacts.count(), 1) + self.assertEqual(contacts.count(), 2) - extended_contact = contacts.get() + extended_contact = contacts.filter(created_by=self.me)[0] self.assertEqual(extended_contact.id, direct_contact.id) self.assertEqual(extended_contact.other_user_id, other_user.id) self.assertFalse(extended_contact.reverse) diff --git a/src/open_inwoner/accounts/views/contacts.py b/src/open_inwoner/accounts/views/contacts.py index 4587c47453..6b88159841 100644 --- a/src/open_inwoner/accounts/views/contacts.py +++ b/src/open_inwoner/accounts/views/contacts.py @@ -40,7 +40,10 @@ def get_queryset(self): | Q(contact_user__isnull=True) ) else: - base_qs = base_qs.filter(contact_user__contact_type=type_filter) + base_qs = base_qs.filter( + Q(contact_user__contact_type=type_filter) + | Q(created_by__contact_type=type_filter) + ) return base_qs def get_context_data(self, **kwargs): diff --git a/src/open_inwoner/accounts/views/invite.py b/src/open_inwoner/accounts/views/invite.py index e06147a516..f87af03adb 100644 --- a/src/open_inwoner/accounts/views/invite.py +++ b/src/open_inwoner/accounts/views/invite.py @@ -29,6 +29,9 @@ def form_valid(self, form): def get_object(self, queryset=None): invite = super().get_object(queryset) + if self.request.user.is_authenticated: + raise Http404(_("U bent al ingelogd.")) + if invite.expired(): self.log_system_action(_("invitation expired"), invite) raise Http404(_("The invitation was expired")) diff --git a/src/open_inwoner/accounts/views/profile.py b/src/open_inwoner/accounts/views/profile.py index c0b6f5629a..ade8b60527 100644 --- a/src/open_inwoner/accounts/views/profile.py +++ b/src/open_inwoner/accounts/views/profile.py @@ -22,7 +22,7 @@ from open_inwoner.utils.views import LogMixin from ..forms import ThemesForm, UserForm -from ..models import Action, User +from ..models import Action, Contact, User class MyProfileView(LogMixin, LoginRequiredMixin, BaseBreadcrumbMixin, FormView): @@ -34,11 +34,6 @@ def crumbs(self): return [(_("Mijn profiel"), reverse("accounts:my_profile"))] def get_context_data(self, **kwargs): - contact_names = [ - f"{contact.first_name} ({contact.get_type_display()})" - for contact in self.request.user.contacts.all()[:3] - ] - context = super().get_context_data(**kwargs) today = date.today() context["anchors"] = [ @@ -46,9 +41,21 @@ def get_context_data(self, **kwargs): ("#overview", _("Persoonlijk overzicht")), ("#files", _("Bestanden")), ] - context["mentor_contacts"] = self.request.user.contacts.filter( - contact_user__contact_type=ContactTypeChoices.begeleider - ) + # List of names of 'mentor' users that are a contact of me + mentor_contacts = [ + str(c.contact_user.get_full_name()) + for c in self.request.user.contacts.filter( + contact_user__contact_type=ContactTypeChoices.begeleider + ) + ] + [ + str(c.created_by.get_full_name()) + for c in Contact.objects.filter( + created_by__contact_type=ContactTypeChoices.begeleider, + contact_user=self.request.user, + ) + ] + + context["mentor_contacts"] = mentor_contacts context["next_action"] = ( Action.objects.connected(self.request.user) .filter(end_date__gte=today, status=StatusChoices.open) @@ -60,10 +67,22 @@ def get_context_data(self, **kwargs): context["action_text"] = _( f"{Action.objects.connected(self.request.user).filter(status=StatusChoices.open).count()} acties staan open." ) - if self.request.user.contacts.count() > 0: + contacts = Contact.objects.get_extended_contacts_for_user(self.request.user) + # Invited contacts + contact_names = [ + f"{contact.first_name} ({contact.get_type_display()})" + for contact in self.request.user.contacts.all()[:3] + ] + # Reverse contacts + contact_names += [ + f"{contact.created_by.first_name} ({contact.created_by.get_contact_type_display()})" + for contact in self.request.user.assigned_contacts.all()[:3] + ] + + if contacts.count() > 0: context[ "contact_text" - ] = f"{', '.join(contact_names)}{'...' if self.request.user.contacts.count() > 3 else ''}" + ] = f"{', '.join(contact_names)}{'...' if contacts.count() > 3 else ''}" else: context["contact_text"] = _("U heeft nog geen contacten.") context["questionnaire_exists"] = QuestionnaireStep.objects.exists() diff --git a/src/open_inwoner/accounts/views/registration.py b/src/open_inwoner/accounts/views/registration.py index 4c1c4daa36..7078e693aa 100644 --- a/src/open_inwoner/accounts/views/registration.py +++ b/src/open_inwoner/accounts/views/registration.py @@ -57,20 +57,6 @@ def add_invitee(self, invite, user): contact.contact_user = user contact.save() - # create reverse contact - reverse_contact, created = Contact.objects.get_or_create( - contact_user=invite.inviter, - created_by=user, - defaults={ - "first_name": invite.inviter.first_name, - "last_name": invite.inviter.last_name, - "email": invite.inviter.email, - }, - ) - if created: - self.request.user = user - self.log_user_action(reverse_contact, _("contact was created")) - class CustomRegistrationView(LogMixin, InviteMixin, RegistrationView): form_class = CustomRegistrationForm diff --git a/src/open_inwoner/components/templates/components/Card/DescriptionCard.html b/src/open_inwoner/components/templates/components/Card/DescriptionCard.html index e203097c22..593f97a601 100644 --- a/src/open_inwoner/components/templates/components/Card/DescriptionCard.html +++ b/src/open_inwoner/components/templates/components/Card/DescriptionCard.html @@ -15,8 +15,6 @@

{% link url text=title %}

{{ object.end_date }}
{% trans "Created by" %}:
{{ object.created_by.get_full_name }}
-
{% trans "Contactpersonen" %}:
-
{{ object.contactperson_list }}
{% endif %} {% link url text=title hide_text=True icon='arrow_forward' button=True transparent=True %} diff --git a/src/open_inwoner/conf/app/csp.py b/src/open_inwoner/conf/app/csp.py index 592090b7f8..ba60e1ba3c 100644 --- a/src/open_inwoner/conf/app/csp.py +++ b/src/open_inwoner/conf/app/csp.py @@ -2,6 +2,10 @@ from ..utils import config +# The Open Forms SDK files might differ from the API domain. +OPEN_FORMS_API_DOMAIN = config("OPEN_FORMS_DOMAIN", "") +OPEN_FORMS_SDK_DOMAIN = OPEN_FORMS_API_DOMAIN + # # Django CSP settings # @@ -13,22 +17,26 @@ "'self'", ) # ideally we'd use BASE_URI but it'd have to be lazy or cause issues CSP_BASE_URI = ("'self'",) -CSP_FONT_SRC = ("'self'",) +CSP_FONT_SRC = ("'self'", OPEN_FORMS_SDK_DOMAIN) CSP_FRAME_ANCESTORS = ["'self'"] CSP_FRAME_SRC = ["'self'"] CSP_OBJECT_SRC = "'none'" CSP_SCRIPT_SRC = ( "'self'", "https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:28992/", + OPEN_FORMS_SDK_DOMAIN, ) # See if the unsafe-eval can be removed.... CSP_STYLE_SRC = ( "'self'", + OPEN_FORMS_SDK_DOMAIN, ) # Fix this. I do not want to have the unsafe-inline here.... CSP_IMG_SRC = ( "'self'", "data:", "https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:28992/", + OPEN_FORMS_SDK_DOMAIN, ) +CSP_CONNECT_SRC = ("'self'", OPEN_FORMS_API_DOMAIN) CSP_UPGRADE_INSECURE_REQUESTS = False # TODO enable on production? CSP_INCLUDE_NONCE_IN = [ diff --git a/src/open_inwoner/conf/base.py b/src/open_inwoner/conf/base.py index 594ab93af1..03d5541231 100644 --- a/src/open_inwoner/conf/base.py +++ b/src/open_inwoner/conf/base.py @@ -155,6 +155,7 @@ "mozilla_django_oidc", "mozilla_django_oidc_db", "sessionprofile", + "openformsclient", # Project applications. "open_inwoner.accounts", "open_inwoner.components", @@ -889,6 +890,8 @@ else: BASE_URL = "https://example.com" +DIGID_MOCK = config("DIGID_MOCK", default=True) +DIGID_ENABLED = config("DIGID_ENABLED", default=True) DIGID_METADATA = config("DIGID_METADATA", "") SSL_CERTIFICATE_PATH = config("SSL_CERTIFICATE_PATH", "") SSL_KEY_PATH = config("SSL_KEY_PATH", "") @@ -934,3 +937,5 @@ MOZILLA_DJANGO_OIDC_DB_CACHE_TIMEOUT = 1 from .app.csp import * # noqa + +SECURE_REFERRER_POLICY = "origin-when-cross-origin" diff --git a/src/open_inwoner/conf/fixtures/custom_csp.json b/src/open_inwoner/conf/fixtures/custom_csp.json index e46133893e..4435e58d37 100644 --- a/src/open_inwoner/conf/fixtures/custom_csp.json +++ b/src/open_inwoner/conf/fixtures/custom_csp.json @@ -30,5 +30,13 @@ "directive": "connect-src", "value": "https://www.google-analytics.com" } + }, + { + "model": "custom_csp.cspsetting", + "pk": 5, + "fields": { + "directive": "connect-src", + "value": "region1.google-analytics.com" + } } ] diff --git a/src/open_inwoner/conf/locale/nl/LC_MESSAGES/django.po b/src/open_inwoner/conf/locale/nl/LC_MESSAGES/django.po index 0331368a72..0b2f1ef2a2 100644 --- a/src/open_inwoner/conf/locale/nl/LC_MESSAGES/django.po +++ b/src/open_inwoner/conf/locale/nl/LC_MESSAGES/django.po @@ -1705,11 +1705,11 @@ msgstr "Tekst over bezoek in de footer." #: configurations/models.py:142 msgid "Footer visiting map" -msgstr "Routetekst in de footer" +msgstr "Bezoekadres url op Google maps" #: configurations/models.py:145 msgid "Visiting address in google maps on the footer section." -msgstr "Route naar de organisatie in de footer, adresgegevens" +msgstr "De link naar het bezoekadres via google maps" #: configurations/models.py:151 msgid "Footer mailing title" diff --git a/src/open_inwoner/conf/production.py b/src/open_inwoner/conf/production.py index aefd1570b8..93c4e0f072 100644 --- a/src/open_inwoner/conf/production.py +++ b/src/open_inwoner/conf/production.py @@ -19,10 +19,15 @@ "open_inwoner.accounts.backends.CustomOIDCBackend", ] +DIGID_MOCK = config("DIGID_MOCK", default=False) if DIGID_METADATA and not DEBUG: AUTHENTICATION_BACKENDS += ["digid_eherkenning.backends.DigiDBackend"] -else: + DIGID_ENABLED = True +elif DIGID_MOCK: AUTHENTICATION_BACKENDS += ["digid_eherkenning.mock.backends.DigiDBackend"] + DIGID_ENABLED = True +else: + DIGID_ENABLED = False # Database performance for db_config in DATABASES.values(): diff --git a/src/open_inwoner/configurations/models.py b/src/open_inwoner/configurations/models.py index ffcbd864cc..12d1c45908 100644 --- a/src/open_inwoner/configurations/models.py +++ b/src/open_inwoner/configurations/models.py @@ -21,6 +21,7 @@ class SiteConfiguration(SingletonModel): ) primary_color = ColorField( verbose_name=_("Primary color"), + default="#FFFFFF", help_text=_("The primary color of the municipality's site"), ) secondary_color = ColorField( diff --git a/src/open_inwoner/js/components/datepicker/index.js b/src/open_inwoner/js/components/datepicker/index.js index ac45430ed8..a17a5267a8 100644 --- a/src/open_inwoner/js/components/datepicker/index.js +++ b/src/open_inwoner/js/components/datepicker/index.js @@ -9,7 +9,7 @@ const instance = flatpickr('.datefield', { let cal = instance.calendarContainer if (cal.querySelectorAll('.flatpickr-clear').length < 1) { let clear = document.createElement('div') - clear.innerText = 'Clear' + clear.innerText = 'Verwijder' clear.classList.add('flatpickr-clear') cal.append(clear) cal.querySelector('.flatpickr-clear').addEventListener('click', () => { diff --git a/src/open_inwoner/pdc/migrations/0042_auto_20220929_1726.py b/src/open_inwoner/pdc/migrations/0042_auto_20220929_1726.py new file mode 100644 index 0000000000..8d6a2f6b24 --- /dev/null +++ b/src/open_inwoner/pdc/migrations/0042_auto_20220929_1726.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.15 on 2022-09-29 15:26 + +from django.db import migrations, models +import openformsclient.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pdc", "0041_auto_20220724_1357"), + ] + + operations = [ + migrations.AddField( + model_name="product", + name="form", + field=openformsclient.models.OpenFormsSlugField( + blank=True, + help_text="Select a form to show this form on the product page. If a form is selected, the link will not be shown.", + verbose_name="Request form", + ), + ), + migrations.AlterField( + model_name="product", + name="link", + field=models.URLField( + blank=True, + default="", + help_text="Action link to request the product.", + verbose_name="Link", + ), + ), + ] diff --git a/src/open_inwoner/pdc/models/product.py b/src/open_inwoner/pdc/models/product.py index 8c9f3d8301..dae86cb035 100644 --- a/src/open_inwoner/pdc/models/product.py +++ b/src/open_inwoner/pdc/models/product.py @@ -8,6 +8,7 @@ from filer.fields.file import FilerFileField from filer.fields.image import FilerImageField +from openformsclient.models import OpenFormsSlugField from ordered_model.models import OrderedModel from open_inwoner.utils.validators import validate_phone_number @@ -57,7 +58,14 @@ class Product(models.Model): verbose_name=_("Link"), blank=True, default="", - help_text=_("Action link to request the product"), + help_text=_("Action link to request the product."), + ) + form = OpenFormsSlugField( + _("Request form"), + blank=True, + help_text=_( + "Select a form to show this form on the product page. If a form is selected, the link will not be shown." + ), ) content = models.TextField( verbose_name=_("Content"), @@ -154,6 +162,10 @@ class Meta: def __str__(self): return self.name + @property + def form_link(self): + return reverse("pdc:product_form", kwargs={"slug": self.slug}) + def get_absolute_url(self, category=None): if not category: return reverse("pdc:product_detail", kwargs={"slug": self.slug}) diff --git a/src/open_inwoner/pdc/urls.py b/src/open_inwoner/pdc/urls.py index 3e3a3fb2c4..208be5e6c8 100644 --- a/src/open_inwoner/pdc/urls.py +++ b/src/open_inwoner/pdc/urls.py @@ -6,6 +6,7 @@ CategoryListView, ProductDetailView, ProductFinderView, + ProductFormView, ) app_name = "pdc" @@ -26,5 +27,16 @@ ProductDetailView.as_view(), name="product_detail", ), + path( + f"{PRODUCT_PATH_NAME}//formulier/", + ProductFormView.as_view(), + name="product_form", + ), + # Required to handle dynamic URL-paths appended by Open Forms. + path( + f"{PRODUCT_PATH_NAME}//formulier/", + ProductFormView.as_view(), + name="product_form", + ), path("finder/", ProductFinderView.as_view(), name="product_finder"), ] diff --git a/src/open_inwoner/pdc/views.py b/src/open_inwoner/pdc/views.py index 9426882a9e..a3ace6b1bb 100644 --- a/src/open_inwoner/pdc/views.py +++ b/src/open_inwoner/pdc/views.py @@ -192,13 +192,40 @@ def get_context_data(self, **kwargs): anchors.append(("#contact", _("Contact"))) if product.related_products.published().exists(): anchors.append(("#see", _("Zie ook"))) - anchors.append(("#share", _("Delen"))) + # anchors.append(("#share", _("Delen"))) disabled for #822 context["anchors"] = anchors context["related_products_start"] = 6 if product.links.exists() else 1 return context +class ProductFormView(BaseBreadcrumbMixin, CategoryBreadcrumbMixin, DetailView): + template_name = "pages/product/form.html" + model = Product + breadcrumb_use_pk = False + no_list = True + + @cached_property + def crumbs(self): + base_list = [(_("Thema's"), reverse("pdc:category_list"))] + base_list += self.get_categories_breadcrumbs(slug_name="theme_slug") + return base_list + [ + (self.get_object().name, self.get_object().get_absolute_url()), + (_("Formulier"), self.request.path), + ] + + def get_context_data(self, **kwargs): + product = self.get_object() + context = super().get_context_data(**kwargs) + + anchors = [ + ("#title", product.name), + ] + + context["anchors"] = anchors + return context + + class ProductFinderView(FormView): template_name = "pages/product/finder.html" form_class = ProductFinderForm diff --git a/src/open_inwoner/plans/managers.py b/src/open_inwoner/plans/managers.py index 20711d142b..b128f8d568 100644 --- a/src/open_inwoner/plans/managers.py +++ b/src/open_inwoner/plans/managers.py @@ -4,8 +4,12 @@ class PlanQuerySet(QuerySet): def connected(self, user): return self.filter( - Q(created_by=user) | Q(contacts__contact_user=user) + Q(created_by=user) + | Q(contacts__contact_user=user) + | Q(contacts__created_by=user) ).distinct() def shared(self, user): - return self.filter(contacts__contact_user=user).distinct() + return self.filter( + Q(contacts__contact_user=user) | Q(contacts__created_by=user) + ).distinct() diff --git a/src/open_inwoner/plans/models.py b/src/open_inwoner/plans/models.py index 09f26b52b6..49e33e2bef 100644 --- a/src/open_inwoner/plans/models.py +++ b/src/open_inwoner/plans/models.py @@ -146,11 +146,16 @@ def get_all_files(self): return self.documents.order_by("-created_on") def get_other_users(self, user=None): - """return list of users participated in the plan with exception of the current user""" + """return list of users participating in the plan with exception of the current user""" contact_user_ids = self.contacts.exclude(contact_user__isnull=True).values_list( "contact_user", flat=True ) - user_ids = list(contact_user_ids) + [self.created_by.id] + created_by_ids = self.contacts.exclude(contact_user__isnull=True).values_list( + "created_by", flat=True + ) + user_ids = list( + set(list(contact_user_ids) + list(created_by_ids) + [self.created_by.id]) + ) if user and user.id in user_ids: user_ids.remove(user.id) diff --git a/src/open_inwoner/plans/tests/test_views.py b/src/open_inwoner/plans/tests/test_views.py index 856bd2ccc1..b928d52178 100644 --- a/src/open_inwoner/plans/tests/test_views.py +++ b/src/open_inwoner/plans/tests/test_views.py @@ -56,6 +56,62 @@ def test_plan_list_filled(self): self.assertEqual(response.status_code, 200) self.assertContains(response, self.plan.title) + def test_plan_detail_contacts(self): + response = self.app.get(self.detail_url, user=self.user) + self.assertContains(response, self.contact_user.get_full_name()) + self.assertContains(response, self.user.get_full_name()) + + response = self.app.get(self.detail_url, user=self.contact_user) + self.assertContains(response, self.user.get_full_name()) + self.assertContains(response, self.contact_user.get_full_name()) + + # Contact for one user, but not the other + # Check if all users can see eachother in the plan + new_user = UserFactory() + new_contact = ContactFactory(contact_user=new_user, created_by=self.user) + self.plan.contacts.add(new_contact) + + response = self.app.get(self.detail_url, user=self.user) + self.assertContains(response, self.contact_user.get_full_name()) + self.assertContains(response, new_user.get_full_name()) + + response = self.app.get(self.detail_url, user=self.contact_user) + self.assertContains(response, self.user.get_full_name()) + self.assertContains(response, new_user.get_full_name()) + + response = self.app.get(self.detail_url, user=new_user) + self.assertContains(response, self.contact_user.get_full_name()) + self.assertContains(response, self.user.get_full_name()) + + new_user.delete() + + # Verify the reverse Contact-relationship + new_user = UserFactory() + new_contact = ContactFactory(created_by=new_user, contact_user=self.user) + self.plan.contacts.add(new_contact) + + response = self.app.get(self.detail_url, user=self.user) + self.assertContains(response, self.contact_user.get_full_name()) + self.assertContains(response, new_user.get_full_name()) + + response = self.app.get(self.detail_url, user=self.contact_user) + self.assertContains(response, self.user.get_full_name()) + self.assertContains(response, new_user.get_full_name()) + + response = self.app.get(self.detail_url, user=new_user) + self.assertContains(response, self.contact_user.get_full_name()) + self.assertContains(response, self.user.get_full_name()) + + new_user.delete() + + # Verify that without being added to the plan the contact isn't visible + new_user = UserFactory() + new_contact = ContactFactory(created_by=new_user, contact_user=self.user) + + response = self.app.get(self.detail_url, user=self.user) + self.assertContains(response, self.contact_user.get_full_name()) + self.assertNotContains(response, new_user.get_full_name()) + def test_plan_contact_can_access(self): response = self.app.get(self.list_url, user=self.contact_user) self.assertEqual(response.status_code, 200) diff --git a/src/open_inwoner/plans/views.py b/src/open_inwoner/plans/views.py index a332dd2e9a..3175149341 100644 --- a/src/open_inwoner/plans/views.py +++ b/src/open_inwoner/plans/views.py @@ -66,6 +66,8 @@ def get_queryset(self): def get_context_data(self, **kwargs): actions = self.object.actions.all() context = super().get_context_data(**kwargs) + context["contact_users"] = self.object.get_other_users(self.request.user) + context["is_creator"] = self.request.user == self.object.created_by context["anchors"] = [ ("#title", self.object.title), ("#goals", _("Doelen")), @@ -121,6 +123,13 @@ def crumbs(self): (_("Bewerken"), reverse("plans:plan_edit", kwargs=self.kwargs)), ] + def get_queryset(self): + return ( + super(PlanEditView, self) + .get_queryset() + .filter(created_by=self.request.user) + ) + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs.update(user=self.request.user) diff --git a/src/open_inwoner/scss/components/Header/AnchorMenu.scss b/src/open_inwoner/scss/components/Header/AnchorMenu.scss index 1b3f16a93b..594c5004c9 100644 --- a/src/open_inwoner/scss/components/Header/AnchorMenu.scss +++ b/src/open_inwoner/scss/components/Header/AnchorMenu.scss @@ -89,7 +89,6 @@ .button { margin: 0; - border-radius: 0; height: var(--row-height); @media (min-width: 768px) { diff --git a/src/open_inwoner/templates/master.html b/src/open_inwoner/templates/master.html index c584a7f6c1..05c528bec8 100644 --- a/src/open_inwoner/templates/master.html +++ b/src/open_inwoner/templates/master.html @@ -1,5 +1,5 @@ {% load static i18n header_tags card_tags footer_tags button_tags notification_tags anchor_menu_tags view_breadcrumbs utils session_tags %} - + {% block title %}{{ site_name }}{% endblock %} diff --git a/src/open_inwoner/templates/pages/plans/detail.html b/src/open_inwoner/templates/pages/plans/detail.html index 7998e1a0c1..c9289037a4 100644 --- a/src/open_inwoner/templates/pages/plans/detail.html +++ b/src/open_inwoner/templates/pages/plans/detail.html @@ -11,11 +11,13 @@

{{ object.title }} {% button_row align="right" %} - {% dropdown icon="settings" secondary=True %} - - {% enddropdown %} + {% if is_creator %} + {% dropdown icon="settings" secondary=True %} + + {% enddropdown %} + {% endif %} {% button href="plans:plan_export" uuid=object.uuid icon="file-pdf" text=_("Exporteer naar PDF") hide_text=True icon_outlined=True transparent=True secondary=True %} {% endbutton_row %} @@ -43,12 +45,10 @@

{% icon "person" outlined=True %}

{% trans "Contactpersonen" %}

- {% for contact in object.contacts.all %} + {% for user in contact_users %}
-

{{ contact.get_name }}

-

{{ contact.function }}

-

{{ contact.phonenumber }}

-

{{ contact.contact_user.email }}

+

{{ user.get_full_name }}

+

{{ user.email }}

{% endfor %}
diff --git a/src/open_inwoner/templates/pages/product/detail.html b/src/open_inwoner/templates/pages/product/detail.html index 232b95d87b..f14435ca2c 100644 --- a/src/open_inwoner/templates/pages/product/detail.html +++ b/src/open_inwoner/templates/pages/product/detail.html @@ -11,7 +11,11 @@ {% block sidebar_content %} {% anchor_menu anchors=anchors desktop=True %} - {% if product.link %} + {% if product.form %} +
  • + {% button href=product.form_link size="big" text=_("Aanvraag starten") primary=True icon="arrow_forward" icon_position="before" %} +
  • + {% elif product.link %}
  • {% button href=product.link size="big" text=_("Aanvraag starten") primary=True icon="arrow_forward" icon_position="before" %}
  • @@ -30,7 +34,11 @@

    {{ object.summary }}

    {{ object.content|ckeditor_content|safe }} - {% if product.link %} + {% if product.form %} + {% button_row mobile=True %} + {% button href=product.form_link text=_("Aanvraag starten") primary=True icon="arrow_forward" icon_position="before" %} + {% endbutton_row %} + {% elif product.link %} {% button_row mobile=True %} {% button href=product.link text=_("Aanvraag starten") primary=True icon="arrow_forward" icon_position="before" %} {% endbutton_row %} diff --git a/src/open_inwoner/templates/pages/product/form.html b/src/open_inwoner/templates/pages/product/form.html new file mode 100644 index 0000000000..d33dac7cc7 --- /dev/null +++ b/src/open_inwoner/templates/pages/product/form.html @@ -0,0 +1,26 @@ +{% extends "pages/product/detail.html" %} +{% load openforms button_tags tag_tags %} + +{% block extra_css %} + + {{ block.super }} + {% openforms_sdk_media %} + +{% endblock %} + +{% block content %} + +

    + {{ object.name }} + {% if request.user.is_staff %} + {% button icon="edit" text=_("Bewerken in de Admin") hide_text=True href="admin:pdc_product_change" object_id=object.pk %} + {% endif %} +

    + {% tag tags=object.tags.all %} +

    {{ object.summary }}

    + + {% if object.form %} + {% openforms_form object.form csp_nonce=request.csp_nonce %} + {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/src/open_inwoner/templates/pages/profile/contacts/list.html b/src/open_inwoner/templates/pages/profile/contacts/list.html index a2287386c7..ca70f14477 100644 --- a/src/open_inwoner/templates/pages/profile/contacts/list.html +++ b/src/open_inwoner/templates/pages/profile/contacts/list.html @@ -25,7 +25,7 @@

    {% for contact in contact_list %} {{ contact.other_user_first_name }} {{ contact.other_user_last_name }} - {{ contact.get_type_display }} + {% if contact.other_user_type == "contact" %}Contactpersoon{% elif contact.other_user_type == "begeleider" %}Begeleider{% elif contact.other_user_type == "organization" %}Organisatie{% endif %} {{ contact.other_user_email|default:"" }} {{ contact.other_user_phonenumber }} {% if contact.is_not_active %}{% icon "check" extra_classes="icon icon--disabled" %}{% else %}{% icon "check" %}{% endif %} diff --git a/src/open_inwoner/templates/pages/profile/me.html b/src/open_inwoner/templates/pages/profile/me.html index 494a8f876f..3ec667b9ad 100644 --- a/src/open_inwoner/templates/pages/profile/me.html +++ b/src/open_inwoner/templates/pages/profile/me.html @@ -28,6 +28,7 @@

    {% icon icon="person_outline" %}
    {{ request.user.get_full_name }}
    + {{ request.user.email }}
    {% if request.user.get_address %}{{ request.user.get_address }}{% else %}-{% endif %}
    diff --git a/src/open_inwoner/urls.py b/src/open_inwoner/urls.py index 2519d6d4df..31cc2fb1af 100644 --- a/src/open_inwoner/urls.py +++ b/src/open_inwoner/urls.py @@ -105,7 +105,7 @@ urlpatterns = [ path("digid/", include("digid_eherkenning.digid_urls")), ] + urlpatterns -else: +elif settings.DIGID_MOCK: urlpatterns = [ path("digid/", include("digid_eherkenning.mock.digid_urls")), path("digid/idp/", include("digid_eherkenning.mock.idp.digid_urls")), diff --git a/src/open_inwoner/utils/context_processors.py b/src/open_inwoner/utils/context_processors.py index f7601c97b4..38160657dd 100644 --- a/src/open_inwoner/utils/context_processors.py +++ b/src/open_inwoner/utils/context_processors.py @@ -11,6 +11,7 @@ def settings(request): "ENVIRONMENT", "SHOW_ALERT", "PROJECT_NAME", + "DIGID_ENABLED", ) config = SiteConfiguration.get_solo()