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 %}
-
- {% button icon="edit" text=_("Bewerken") href="plans:plan_edit" uuid=object.uuid icon_outlined=True transparent=True %}
-
- {% enddropdown %}
+ {% if is_creator %}
+ {% dropdown icon="settings" secondary=True %}
+
+ {% button icon="edit" text=_("Bewerken") href="plans:plan_edit" uuid=object.uuid icon_outlined=True transparent=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" %}
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 %}
+
+ {% elif product.link %}
@@ -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.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()