+
{{ title }}
+ {% else %}
+
+
+
{{ title }}
+ {% endif %}
+
{{ description }}
+ {% if object.end_date %}
+
+
{% trans "Einddatum" %}:
+
{{ object.end_date }}
+
{% trans "Created by" %}:
+
{{ object.created_by.get_full_name }}
+
+ {% endif %}
+
+
+ {% icon icon="arrow_forward" icon_position="after" primary=True outlined=True %}
+
+
+
+ {# end of card__content #}
+
+
+
diff --git a/src/open_inwoner/components/templates/components/Header/PrimaryNavigation.html b/src/open_inwoner/components/templates/components/Header/PrimaryNavigation.html
index 36667f6204..49e739f26f 100644
--- a/src/open_inwoner/components/templates/components/Header/PrimaryNavigation.html
+++ b/src/open_inwoner/components/templates/components/Header/PrimaryNavigation.html
@@ -46,7 +46,14 @@
{% endif %}
{% if show_plans %}
- {% link text=_('Samenwerken') href='plans:plan_list' icon="people" icon_outlined=True icon_position="before" %}
+ {% link text=_('Samenwerken') href='plans:plan_list' icon="people" icon_outlined=True icon_position="before" %}
+ {% with request.user.get_plan_contact_new_count as plan_count %}
+ {% if plan_count %}
+ {% with "("|addstr:plan_count|addstr:")" as plan_count %}
+ {% link text=plan_count href='plans:plan_list' secondary=True %}
+ {% endwith %}
+ {% endif %}
+ {% endwith %}
{% endif %}
{% endif %}
diff --git a/src/open_inwoner/components/templatetags/card_tags.py b/src/open_inwoner/components/templatetags/card_tags.py
index 4d1caa32cf..3ddfe5fd20 100644
--- a/src/open_inwoner/components/templatetags/card_tags.py
+++ b/src/open_inwoner/components/templatetags/card_tags.py
@@ -90,6 +90,25 @@ def description_card(title, description, url, **kwargs):
return kwargs
+@register.inclusion_tag("components/Card/ProductCard.html")
+def product_card(description, url, **kwargs):
+ """
+ Renders a card with or without an image prepopulated based on `product`.
+
+ Usage:
+ {% product_card title=product.title description=product.intro url=product.get_absolute_url %}
+
+ Available options:
+ + description: string | The description that needs to be displayed.
+ + url: string | The url that the card should point to.
+ - title: string | The title of the card that may be displayed if there is no image.
+ - object: any | The object that needs to render aditional data.
+ - image: FilerImageField | an image that should be used.
+ """
+ kwargs.update(description=description, url=url)
+ return kwargs
+
+
@register.inclusion_tag("components/Card/CardContainer.html")
def card_container(categories=[], subcategories=[], products=[], plans=[], **kwargs):
"""
diff --git a/src/open_inwoner/components/templatetags/link_tags.py b/src/open_inwoner/components/templatetags/link_tags.py
index fa661d6d8f..e8b53329b9 100644
--- a/src/open_inwoner/components/templatetags/link_tags.py
+++ b/src/open_inwoner/components/templatetags/link_tags.py
@@ -2,6 +2,8 @@
from django.urls import NoReverseMatch, reverse
from django.utils.html import format_html
+from furl import furl
+
register = template.Library()
@@ -112,3 +114,13 @@ def get_classes():
kwargs["href"] = get_href()
kwargs["text"] = text
return kwargs
+
+
+@register.filter
+def addnexturl(href, next_url):
+ """
+ Concatenates href & next_url.
+ """
+ f = furl(href)
+ f.args["next"] = next_url
+ return f.url
diff --git a/src/open_inwoner/components/templatetags/product_tags.py b/src/open_inwoner/components/templatetags/product_tags.py
index 3885267787..a035693858 100644
--- a/src/open_inwoner/components/templatetags/product_tags.py
+++ b/src/open_inwoner/components/templatetags/product_tags.py
@@ -2,6 +2,8 @@
from django.urls import NoReverseMatch, reverse
from django.utils.translation import gettext as _
+from open_inwoner.utils.ckeditor import get_product_rendered_content
+
register = template.Library()
@@ -36,3 +38,19 @@ def product_finder(
configurable_text=context["configurable_text"],
)
return kwargs
+
+
+@register.filter("product_ckeditor_content")
+def product_ckeditor_content(product):
+ """
+ Returns rendered content from ckeditor's textarea field specifically for a product.
+
+ Usage:
+ {{ object|product_ckeditor_content }}
+
+ Variables:
+ + product: Product | The product object
+ """
+ rendered_content = get_product_rendered_content(product)
+
+ return rendered_content
diff --git a/src/open_inwoner/conf/base.py b/src/open_inwoner/conf/base.py
index a8fc580ff0..9258cbe413 100644
--- a/src/open_inwoner/conf/base.py
+++ b/src/open_inwoner/conf/base.py
@@ -78,7 +78,7 @@
},
"axes": {
"BACKEND": "django_redis.cache.RedisCache",
- "LOCATION": f"redis://{config('CACHE_AXES', 'localhost:6379/0')}",
+ "LOCATION": f"redis://{config('CACHE_DEFAULT', 'localhost:6379/0')}",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"IGNORE_EXCEPTIONS": True,
@@ -86,7 +86,7 @@
},
"oidc": {
"BACKEND": "django_redis.cache.RedisCache",
- "LOCATION": f"redis://{config('CACHE_OIDC', 'localhost:6379/0')}",
+ "LOCATION": f"redis://{config('CACHE_DEFAULT', 'localhost:6379/0')}",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"IGNORE_EXCEPTIONS": True,
@@ -1027,6 +1027,9 @@
#
TWO_FACTOR_FORCE_OTP_ADMIN = config("TWO_FACTOR_FORCE_OTP_ADMIN", default=not DEBUG)
TWO_FACTOR_PATCH_ADMIN = config("TWO_FACTOR_PATCH_ADMIN", default=True)
+ADMIN_INDEX_DISPLAY_DROP_DOWN_MENU_CONDITION_FUNCTION = (
+ "open_inwoner.utils.django_two_factor_auth.should_display_dropdown_menu"
+)
# file upload limits
MIN_UPLOAD_SIZE = 1 # in bytes
diff --git a/src/open_inwoner/conf/fixtures/django-admin-index.json b/src/open_inwoner/conf/fixtures/django-admin-index.json
index bdf7f3fe52..86cf91a760 100644
--- a/src/open_inwoner/conf/fixtures/django-admin-index.json
+++ b/src/open_inwoner/conf/fixtures/django-admin-index.json
@@ -120,7 +120,7 @@
{
"model": "admin_index.appgroup",
"fields": {
- "order": 7,
+ "order": 9,
"name": "Overige / Diverse",
"slug": "overige-diverse",
"models": [
@@ -162,7 +162,7 @@
{
"model": "admin_index.appgroup",
"fields": {
- "order": 6,
+ "order": 7,
"name": "Configuratie",
"slug": "configuratie",
"models": [
@@ -182,61 +182,13 @@
"flatpages",
"flatpage"
],
- [
- "haalcentraal",
- "haalcentraalconfig"
- ],
[
"mail_editor",
"mailtemplate"
],
- [
- "mozilla_django_oidc_db",
- "openidconnectconfig"
- ],
- [
- "notifications_api_common",
- "notificationsconfig"
- ],
- [
- "notifications_api_common",
- "subscription"
- ],
- [
- "openformsclient",
- "configuration"
- ],
- [
- "openzaak",
- "catalogusconfig"
- ],
- [
- "openzaak",
- "openzaakconfig"
- ],
- [
- "openzaak",
- "usercasestatusnotification"
- ],
- [
- "openzaak",
- "zaaktypeconfig"
- ],
- [
- "openzaak",
- "zaaktypeinformatieobjecttypeconfig"
- ],
[
"sites",
"site"
- ],
- [
- "zgw_consumers",
- "nlxconfig"
- ],
- [
- "zgw_consumers",
- "service"
]
]
}
@@ -333,6 +285,72 @@
]
}
},
+{
+ "model": "admin_index.appgroup",
+ "fields": {
+ "order": 6,
+ "name": "Koppelingen",
+ "slug": "koppelingen",
+ "models": [
+ [
+ "haalcentraal",
+ "haalcentraalconfig"
+ ],
+ [
+ "mozilla_django_oidc_db",
+ "openidconnectconfig"
+ ],
+ [
+ "notifications_api_common",
+ "notificationsconfig"
+ ],
+ [
+ "notifications_api_common",
+ "subscription"
+ ],
+ [
+ "openformsclient",
+ "configuration"
+ ],
+ [
+ "openzaak",
+ "catalogusconfig"
+ ],
+ [
+ "openzaak",
+ "openzaakconfig"
+ ],
+ [
+ "openzaak",
+ "usercaseinfoobjectnotification"
+ ],
+ [
+ "openzaak",
+ "usercasestatusnotification"
+ ],
+ [
+ "openzaak",
+ "zaaktypeconfig"
+ ],
+ [
+ "openzaak",
+ "zaaktypeinformatieobjecttypeconfig"
+ ],
+ [
+ "zgw_consumers",
+ "certificate"
+ ],
+ [
+ "zgw_consumers",
+ "nlxconfig"
+ ],
+ [
+ "zgw_consumers",
+ "service"
+ ]
+ ]
+ }
+},
{
"model": "admin_index.applink",
"fields": {
diff --git a/src/open_inwoner/haalcentraal/utils.py b/src/open_inwoner/haalcentraal/utils.py
index e4099e0da5..d82b59fee6 100644
--- a/src/open_inwoner/haalcentraal/utils.py
+++ b/src/open_inwoner/haalcentraal/utils.py
@@ -36,13 +36,10 @@ def fetch_brp_data(instance, brp_version):
"burgerservicenummer": [instance.bsn],
},
request_kwargs=dict(
- headers={"Accept": "application/hal+json"}, verify=False
+ headers={"Accept": "application/json"}, verify=False
),
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return {}
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return {}
@@ -68,10 +65,7 @@ def fetch_brp_data(instance, brp_version):
verify=False,
),
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return {}
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return {}
diff --git a/src/open_inwoner/js/components/cases/index.js b/src/open_inwoner/js/components/cases/index.js
new file mode 100644
index 0000000000..967811e34d
--- /dev/null
+++ b/src/open_inwoner/js/components/cases/index.js
@@ -0,0 +1,16 @@
+class DisableSubmitButton {
+ constructor(form) {
+ this.form = form
+ this.form.addEventListener('submit', this.disableButton.bind(this))
+ }
+
+ disableButton() {
+ const submitButton = this.form.querySelector('button[type="submit"]')
+ submitButton.setAttribute('disabled', 'true')
+ }
+}
+
+const caseDocumentForms = document.querySelectorAll('#document-upload')
+;[...caseDocumentForms].forEach(
+ (caseDocumentForm) => new DisableSubmitButton(caseDocumentForm)
+)
diff --git a/src/open_inwoner/js/components/index.js b/src/open_inwoner/js/components/index.js
index fa7a189e71..833ac6a9cd 100644
--- a/src/open_inwoner/js/components/index.js
+++ b/src/open_inwoner/js/components/index.js
@@ -6,6 +6,7 @@ import './anchor-menu'
import './autocomplete-search'
import './autocomplete'
import './autosumbit'
+import './cases'
import './confirmation'
import './contacts'
import './datepicker'
diff --git a/src/open_inwoner/js/components/map/index.js b/src/open_inwoner/js/components/map/index.js
index dd3108593c..0ccfb1e0b0 100644
--- a/src/open_inwoner/js/components/map/index.js
+++ b/src/open_inwoner/js/components/map/index.js
@@ -3,6 +3,19 @@ import 'leaflet'
import { RD_CRS } from './rd'
import { isMobile } from '../../lib/device/is-mobile'
+/**
+ * Returns an escaped variable.
+ * @param {string} textVariable
+ * @return {string}
+ */
+function escapeVariableText(textVariable) {
+ if (textVariable) {
+ return escapeHTML(textVariable)
+ } else {
+ return ''
+ }
+}
+
/** @type {NodeListOf
} All the leaflet maps. */
const LEAFLET_MAPS = document.querySelectorAll('.map__leaflet')
@@ -74,31 +87,50 @@ class Map {
* @return {string}
*/
featureToHTML(feature) {
- const { name, location_url, ...properties } = feature.properties
- const displayName = name ? escapeHTML(name) : ''
- const locationDetailView = location_url ? escapeHTML(location_url) : ''
+ const {
+ name,
+ location_url,
+ address_line_1,
+ address_line_2,
+ phonenumber,
+ email,
+ ...properties
+ } = feature.properties
+
+ const displayName = escapeVariableText(name)
+ const locationDetailView = escapeVariableText(location_url)
+ const displayAddress1 = escapeVariableText(address_line_1)
+ const displayAddress2 = escapeVariableText(address_line_2)
+ const displayPhonenumber = escapeVariableText(phonenumber)
+ const displayEmail = escapeVariableText(email)
let title = ''
- let finalHTML = ``
if (locationDetailView) {
- title = `${displayName}`
+ title = `
+
+ ${displayName}
+
+ `
} else {
title = displayName
}
- Object.entries(properties).forEach((property) => {
- finalHTML += `${escapeHTML(property[1])}
`
- })
-
return `
-
-
- ${title}
-
-
-
- ${finalHTML}
-
+
+
+ ${title}
+
+
+
+
${displayAddress1}
+
${displayAddress2}
+
+ ${displayPhonenumber}
+
+
+ ${displayEmail}
+
+
`
}
}
diff --git a/src/open_inwoner/openzaak/cases.py b/src/open_inwoner/openzaak/cases.py
index 91a78486b3..64c68ae1ca 100644
--- a/src/open_inwoner/openzaak/cases.py
+++ b/src/open_inwoner/openzaak/cases.py
@@ -44,10 +44,7 @@ def fetch_cases(user_bsn: str, max_cases: Optional[int] = 100) -> List[Zaak]:
},
},
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -65,10 +62,7 @@ def fetch_single_case(case_uuid: str) -> Optional[Zaak]:
try:
response = client.retrieve("zaak", uuid=case_uuid)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
@@ -88,10 +82,7 @@ def fetch_single_case_information_object(url: str) -> Optional[ZaakInformatieObj
try:
response = client.retrieve("zaakinformatieobject", url=url)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
@@ -104,10 +95,7 @@ def fetch_case_by_url_no_cache(case_url: str) -> Optional[Zaak]:
client = build_client("zaak")
try:
response = client.retrieve("zaak", url=case_url)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
@@ -130,10 +118,7 @@ def fetch_case_information_objects(case_url: str) -> List[ZaakInformatieObject]:
"params": {"zaak": case_url},
},
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -155,10 +140,7 @@ def fetch_status_history_no_cache(case_url: str) -> List[Status]:
try:
response = client.list("status", request_kwargs={"params": {"zaak": case_url}})
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -176,10 +158,7 @@ def fetch_single_status(status_url: str) -> Optional[Status]:
try:
response = client.retrieve("status", url=status_url)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
@@ -213,10 +192,7 @@ def fetch_case_roles(
"rol",
request_kwargs={"params": params},
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -270,10 +246,7 @@ def fetch_case_information_objects_for_case_and_info(
},
},
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -291,10 +264,7 @@ def fetch_single_result(result_url: str) -> Optional[Resultaat]:
try:
response = client.retrieve("result", url=result_url)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
@@ -313,10 +283,7 @@ def connect_case_with_document(case_url: str, document_url: str) -> Optional[dic
response = client.create(
"zaakinformatieobject", {"zaak": case_url, "informatieobject": document_url}
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
diff --git a/src/open_inwoner/openzaak/catalog.py b/src/open_inwoner/openzaak/catalog.py
index 55922642e2..0685fc984e 100644
--- a/src/open_inwoner/openzaak/catalog.py
+++ b/src/open_inwoner/openzaak/catalog.py
@@ -30,10 +30,7 @@ def fetch_status_types_no_cache(case_type_url: str) -> List[StatusType]:
"statustype",
request_kwargs={"params": {"zaaktype": case_type_url}},
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -56,10 +53,7 @@ def fetch_result_types_no_cache(case_type_url: str) -> List[ResultaatType]:
"resultaattype",
request_kwargs={"params": {"zaaktype": case_type_url}},
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -79,10 +73,7 @@ def fetch_single_status_type(status_type_url: str) -> Optional[StatusType]:
try:
response = client.retrieve("statustype", url=status_type_url)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
@@ -104,10 +95,7 @@ def fetch_zaaktypes_no_cache() -> List[ZaakType]:
try:
response = get_paginated_results(client, "zaaktype")
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -138,10 +126,7 @@ def fetch_case_types_by_identification_no_cache(
"zaaktype",
request_kwargs={"params": params},
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -174,10 +159,7 @@ def fetch_single_case_type(case_type_url: str) -> Optional[ZaakType]:
try:
response = client.retrieve("zaaktype", url=case_type_url)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
@@ -200,10 +182,7 @@ def fetch_catalogs_no_cache() -> List[Catalogus]:
client,
"catalogus",
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
@@ -228,10 +207,7 @@ def fetch_single_information_object_type(
response = client.retrieve(
"informatieobjecttype", url=information_object_type_url
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
diff --git a/src/open_inwoner/openzaak/documents.py b/src/open_inwoner/openzaak/documents.py
index 2cab4e507d..7e5e6c5c9a 100644
--- a/src/open_inwoner/openzaak/documents.py
+++ b/src/open_inwoner/openzaak/documents.py
@@ -14,10 +14,7 @@
from open_inwoner.openzaak.api_models import InformatieObject
from open_inwoner.openzaak.clients import build_client
-from open_inwoner.openzaak.models import (
- OpenZaakConfig,
- ZaakTypeInformatieObjectTypeConfig,
-)
+from open_inwoner.openzaak.models import OpenZaakConfig
from .utils import cache as cache_result
@@ -50,10 +47,7 @@ def _fetch_single_information_object(
response = client.retrieve("enkelvoudiginformatieobject", url=url)
else:
response = client.retrieve("enkelvoudiginformatieobject", uuid=uuid)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
@@ -125,10 +119,7 @@ def upload_document(
try:
response = client.create("enkelvoudiginformatieobject", document_body)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return
diff --git a/src/open_inwoner/openzaak/formapi.py b/src/open_inwoner/openzaak/formapi.py
index 2afe0ae6a9..755249de8a 100644
--- a/src/open_inwoner/openzaak/formapi.py
+++ b/src/open_inwoner/openzaak/formapi.py
@@ -38,10 +38,7 @@ def fetch_open_submissions(bsn: str) -> List[OpenSubmission]:
"opensubmission",
request_kwargs={"params": {"bsn": bsn}},
)
- except RequestException as e:
- logger.exception("exception while making request", exc_info=e)
- return []
- except ClientError as e:
+ except (RequestException, ClientError) as e:
logger.exception("exception while making request", exc_info=e)
return []
diff --git a/src/open_inwoner/openzaak/notifications.py b/src/open_inwoner/openzaak/notifications.py
index 613d43891e..4dcbe9ea29 100644
--- a/src/open_inwoner/openzaak/notifications.py
+++ b/src/open_inwoner/openzaak/notifications.py
@@ -83,7 +83,8 @@ def handle_zaken_notification(notification: Notification):
if not roles:
log_system_action(
f"ignored {r} notification: cannot retrieve rollen for case {case_url}",
- log_level=logging.ERROR,
+ # NOTE this used to be logging.ERROR, but as this is also our first call we get a lot of 403 "Niet geautoriseerd voor zaaktype"
+ log_level=logging.INFO,
)
return
diff --git a/src/open_inwoner/openzaak/tests/test_notification_zaak_infoobject.py b/src/open_inwoner/openzaak/tests/test_notification_zaak_infoobject.py
index 4f0283dea1..6edfea7c77 100644
--- a/src/open_inwoner/openzaak/tests/test_notification_zaak_infoobject.py
+++ b/src/open_inwoner/openzaak/tests/test_notification_zaak_infoobject.py
@@ -98,7 +98,7 @@ def test_zio_bails_when_no_roles_found_for_case(self, m, mock_handle: Mock):
self.assertTimelineLog(
"ignored zaakinformatieobject notification: cannot retrieve rollen for case https://",
lookup=Lookups.startswith,
- level=logging.ERROR,
+ level=logging.INFO,
)
def test_zio_bails_when_no_emailable_users_are_found_for_roles(
diff --git a/src/open_inwoner/openzaak/tests/test_notification_zaak_status.py b/src/open_inwoner/openzaak/tests/test_notification_zaak_status.py
index 5f882eee50..aec3e39357 100644
--- a/src/open_inwoner/openzaak/tests/test_notification_zaak_status.py
+++ b/src/open_inwoner/openzaak/tests/test_notification_zaak_status.py
@@ -94,7 +94,7 @@ def test_status_bails_when_no_roles_found_for_case(self, m, mock_handle: Mock):
self.assertTimelineLog(
"ignored status notification: cannot retrieve rollen for case https://",
lookup=Lookups.startswith,
- level=logging.ERROR,
+ level=logging.INFO,
)
def test_status_bails_when_no_emailable_users_are_found_for_roles(
diff --git a/src/open_inwoner/pdc/migrations/0055_alter_product_content.py b/src/open_inwoner/pdc/migrations/0055_alter_product_content.py
new file mode 100644
index 0000000000..db32a93935
--- /dev/null
+++ b/src/open_inwoner/pdc/migrations/0055_alter_product_content.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.2.15 on 2023-03-21 07:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("pdc", "0054_alter_organization_logo"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="product",
+ name="content",
+ field=models.TextField(
+ help_text="Product content with build-in WYSIWYG editor. By adding '[CTABUTTON]' you can embed a cta-button for linking to the defined form or link",
+ verbose_name="Content",
+ ),
+ ),
+ ]
diff --git a/src/open_inwoner/pdc/models/product.py b/src/open_inwoner/pdc/models/product.py
index db031498bd..8d304ead94 100644
--- a/src/open_inwoner/pdc/models/product.py
+++ b/src/open_inwoner/pdc/models/product.py
@@ -88,7 +88,9 @@ class Product(models.Model):
)
content = models.TextField(
verbose_name=_("Content"),
- help_text=_("Product content with build-in WYSIWYG editor"),
+ help_text=_(
+ "Product content with build-in WYSIWYG editor. By adding '[CTABUTTON]' you can embed a cta-button for linking to the defined form or link"
+ ),
)
categories = models.ManyToManyField(
"pdc.Category",
diff --git a/src/open_inwoner/pdc/tests/test_product.py b/src/open_inwoner/pdc/tests/test_product.py
index cc6f6c549d..4dae1e41a1 100644
--- a/src/open_inwoner/pdc/tests/test_product.py
+++ b/src/open_inwoner/pdc/tests/test_product.py
@@ -147,3 +147,64 @@ def test_product_detail_shows_product_faq(self):
self.assertTrue(response.pyquery('.anchor-menu a[href="#faq"]'))
# check if the menu link target
self.assertTrue(response.pyquery("#faq"))
+
+
+class TestProductContent(WebTest):
+ def test_button_is_rendered_inside_content_when_link_and_cta_exist(self):
+ product = ProductFactory(
+ content="Some content [CTABUTTON]", link="http://www.example.com"
+ )
+
+ response = self.app.get(
+ reverse("pdc:product_detail", kwargs={"slug": product.slug})
+ )
+ cta_button = response.pyquery(".grid__main")[0].find_class("cta-button")
+
+ self.assertTrue(cta_button)
+ self.assertIn(product.link, cta_button[0].values())
+
+ def test_button_is_rendered_inside_content_when_form_and_cta_exist(self):
+ product = ProductFactory(content="Some content [CTABUTTON]", form="demo")
+
+ response = self.app.get(
+ reverse("pdc:product_detail", kwargs={"slug": product.slug})
+ )
+ cta_button = response.pyquery(".grid__main")[0].find_class("cta-button")
+
+ self.assertTrue(cta_button)
+ self.assertIn(product.form_link, cta_button[0].values())
+
+ def test_button_is_rendered_inside_content_when_form_and_link_and_cta_exist(self):
+ product = ProductFactory(
+ content="Some content [CTABUTTON]",
+ link="http://www.example.com",
+ form="demo",
+ )
+
+ response = self.app.get(
+ reverse("pdc:product_detail", kwargs={"slug": product.slug})
+ )
+ cta_button = response.pyquery(".grid__main")[0].find_class("cta-button")
+
+ self.assertTrue(cta_button)
+ self.assertIn(product.link, cta_button[0].values())
+
+ def test_button_is_not_rendered_inside_content_when_no_cta(self):
+ product = ProductFactory(content="Some content", link="http://www.example.com")
+
+ response = self.app.get(
+ reverse("pdc:product_detail", kwargs={"slug": product.slug})
+ )
+ cta_button = response.pyquery(".grid__main")[0].find_class("cta-button")
+
+ self.assertFalse(cta_button)
+
+ def test_button_is_not_rendered_inside_content_when_no_form_or_link(self):
+ product = ProductFactory(content="Some content [CTABUTTON]")
+
+ response = self.app.get(
+ reverse("pdc:product_detail", kwargs={"slug": product.slug})
+ )
+ cta_button = response.pyquery(".grid__main")[0].find_class("cta-button")
+
+ self.assertFalse(cta_button)
diff --git a/src/open_inwoner/pdc/views.py b/src/open_inwoner/pdc/views.py
index 68be083cd0..5ddcfef7e7 100644
--- a/src/open_inwoner/pdc/views.py
+++ b/src/open_inwoner/pdc/views.py
@@ -60,7 +60,7 @@ def page_title(self):
def get_context_data(self, **kwargs):
config = SiteConfiguration.get_solo()
- limit = 3 if self.request.user.is_authenticated else 4
+ limit = 4
kwargs.update(categories=Category.objects.published().order_by("name")[:limit])
kwargs.update(product_locations=ProductLocation.objects.all()[:1000])
kwargs.update(
@@ -86,7 +86,7 @@ def get_context_data(self, **kwargs):
and self.request.user.selected_themes.exists()
):
kwargs.update(
- categories=self.request.user.selected_themes.order_by("name")[:3]
+ categories=self.request.user.selected_themes.order_by("name")[:limit]
)
elif highlighted_categories:
kwargs.update(categories=highlighted_categories)
diff --git a/src/open_inwoner/plans/migrations/0013_auto_20230223_1115.py b/src/open_inwoner/plans/migrations/0013_auto_20230223_1115.py
index 7a756d545e..b14a9a0440 100644
--- a/src/open_inwoner/plans/migrations/0013_auto_20230223_1115.py
+++ b/src/open_inwoner/plans/migrations/0013_auto_20230223_1115.py
@@ -6,6 +6,7 @@
class Migration(migrations.Migration):
dependencies = [
+ ("accounts", "0055_user_image"),
("plans", "0012_remove_actiontemplate_goal"),
]
diff --git a/src/open_inwoner/plans/migrations/0014_auto_20230306_1017.py b/src/open_inwoner/plans/migrations/0014_auto_20230306_1017.py
new file mode 100644
index 0000000000..cf2a98785f
--- /dev/null
+++ b/src/open_inwoner/plans/migrations/0014_auto_20230306_1017.py
@@ -0,0 +1,70 @@
+# Generated by Django 3.2.15 on 2023-03-06 09:17
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("accounts", "0055_user_image"),
+ ("plans", "0013_auto_20230223_1115"),
+ ]
+
+ # https://docs.djangoproject.com/en/3.2/howto/writing-migrations/#changing-a-manytomanyfield-to-use-a-through-model
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ database_operations=[
+ migrations.RunSQL(
+ sql="ALTER TABLE plans_plan_plan_contacts RENAME TO plans_plancontact",
+ reverse_sql="ALTER TABLE plans_plancontact RENAME TO plans_plan_plan_contacts",
+ ),
+ ],
+ state_operations=[
+ migrations.CreateModel(
+ name="PlanContact",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Contact",
+ ),
+ ),
+ (
+ "plan",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="plans.plan",
+ verbose_name="Plan",
+ ),
+ ),
+ ],
+ ),
+ migrations.AlterField(
+ model_name="plan",
+ name="plan_contacts",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="The contact that will help you with this plan.",
+ related_name="plans",
+ through="plans.PlanContact",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Contacts",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/src/open_inwoner/plans/migrations/0015_plancontact_notify_new.py b/src/open_inwoner/plans/migrations/0015_plancontact_notify_new.py
new file mode 100644
index 0000000000..7785dd23a3
--- /dev/null
+++ b/src/open_inwoner/plans/migrations/0015_plancontact_notify_new.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.15 on 2023-03-06 09:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("plans", "0014_auto_20230306_1017"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="plancontact",
+ name="notify_new",
+ field=models.BooleanField(default=False, verbose_name="Notify contact"),
+ ),
+ ]
diff --git a/src/open_inwoner/plans/migrations/0016_alter_plancontact_notify_new.py b/src/open_inwoner/plans/migrations/0016_alter_plancontact_notify_new.py
new file mode 100644
index 0000000000..71faed0d75
--- /dev/null
+++ b/src/open_inwoner/plans/migrations/0016_alter_plancontact_notify_new.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.15 on 2023-03-13 10:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("plans", "0015_plancontact_notify_new"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="plancontact",
+ name="notify_new",
+ field=models.BooleanField(default=True, verbose_name="Notify contact"),
+ ),
+ ]
diff --git a/src/open_inwoner/plans/models.py b/src/open_inwoner/plans/models.py
index ca6a75fb48..c0d01b03f2 100644
--- a/src/open_inwoner/plans/models.py
+++ b/src/open_inwoner/plans/models.py
@@ -90,6 +90,16 @@ class ActionTemplate(models.Model):
)
+class PlanContact(models.Model):
+ plan = models.ForeignKey(
+ "plans.Plan", verbose_name=_("Plan"), on_delete=models.CASCADE
+ )
+ user = models.ForeignKey(
+ "accounts.User", verbose_name=_("Contact"), on_delete=models.CASCADE
+ )
+ notify_new = models.BooleanField(verbose_name=_("Notify contact"), default=True)
+
+
class Plan(models.Model):
uuid = models.UUIDField(verbose_name=_("UUID"), default=uuid4, unique=True)
title = models.CharField(
@@ -116,6 +126,7 @@ class Plan(models.Model):
related_name="plans",
blank=True,
help_text=_("The contact that will help you with this plan."),
+ through=PlanContact,
)
created_by = models.ForeignKey(
"accounts.User", verbose_name=_("Created by"), on_delete=models.CASCADE
diff --git a/src/open_inwoner/plans/tests/test_data_migrations.py b/src/open_inwoner/plans/tests/test_data_migrations.py
index 752f393e77..8dab873a67 100644
--- a/src/open_inwoner/plans/tests/test_data_migrations.py
+++ b/src/open_inwoner/plans/tests/test_data_migrations.py
@@ -44,3 +44,41 @@ def setUpBeforeMigration(self, apps):
def test_plan_contacts_is_updated_with_existing_contact(self):
self.plan.refresh_from_db()
self.assertEqual(list(self.plan.plan_contacts.all()), [self.contact_user])
+
+
+class PlanContactThroughModelMigrationTests(TestMigrations):
+ app = "plans"
+ migrate_from = "0013_auto_20230223_1115"
+ migrate_to = "0015_plancontact_notify_new"
+
+ extra_migrate_from = [("accounts", "0055_user_image")]
+
+ def setUpBeforeMigration(self, apps):
+ UserModel = apps.get_model("accounts", "User")
+ self.user = UserModel.objects.create(
+ email="user@example.com",
+ )
+ self.other_user = UserModel.objects.create(
+ email="other_user@example.com",
+ )
+
+ PlanModel = apps.get_model("plans", "Plan")
+ self.plan = PlanModel.objects.create(
+ title="A title",
+ end_date="2021-01-10",
+ created_by=self.user,
+ )
+ self.plan.plan_contacts.add(self.other_user)
+
+ def test_plan_contacts_still_exist(self):
+ UserModel = self.apps.get_model("accounts", "User")
+ PlanModel = self.apps.get_model("plans", "Plan")
+ PlanContact = self.apps.get_model("plans", "PlanContact")
+
+ other_user = UserModel.objects.get(id=self.other_user.id)
+ plan = PlanModel.objects.get(id=self.plan.id)
+ self.assertEqual(list(plan.plan_contacts.all()), [other_user])
+
+ # check we don't notify existing contacts
+ contact = PlanContact.objects.get()
+ self.assertEqual(contact.notify_new, False)
diff --git a/src/open_inwoner/plans/tests/test_views.py b/src/open_inwoner/plans/tests/test_views.py
index 7ab3cb2b15..3df5be98c7 100644
--- a/src/open_inwoner/plans/tests/test_views.py
+++ b/src/open_inwoner/plans/tests/test_views.py
@@ -15,7 +15,7 @@
from open_inwoner.accounts.tests.test_action_views import ActionsPlaywrightTests
from open_inwoner.utils.tests.playwright import multi_browser
-from ..models import Plan
+from ..models import Plan, PlanContact
from .factories import ActionTemplateFactory, PlanFactory, PlanTemplateFactory
@@ -51,6 +51,10 @@ def setUp(self) -> None:
"plans:plan_action_edit",
kwargs={"plan_uuid": self.plan.uuid, "uuid": self.action.uuid},
)
+ self.action_history_url = reverse(
+ "plans:plan_action_history",
+ kwargs={"plan_uuid": self.plan.uuid, "uuid": self.action.uuid},
+ )
self.action_edit_status_url = reverse(
"plans:plan_action_edit_status",
kwargs={"plan_uuid": self.plan.uuid, "uuid": self.action.uuid},
@@ -77,6 +81,10 @@ def test_creator_is_added_when_create_plan(self):
self.assertEqual(created_plan.plan_contacts.get(), self.user)
self.assertEqual(created_plan.created_by, self.user)
+ contact = PlanContact.objects.get(plan=created_plan)
+ self.assertEqual(contact.user, self.user)
+ self.assertEqual(contact.notify_new, True)
+
def test_plan_list_filled(self):
response = self.app.get(self.list_url, user=self.user)
self.assertEqual(response.status_code, 200)
@@ -484,6 +492,13 @@ def test_plan_action_edit_not_changed(self):
# no notification is sent
self.assertEqual(len(mail.outbox), 0)
+ def test_plan_actions_history_breadcrumbs(self):
+ response = self.app.get(self.action_history_url, user=self.user)
+ crumbs = response.pyquery(".breadcrumbs__list-item")
+ self.assertIn(_("Samenwerking"), crumbs[1].text_content())
+ self.assertIn(self.plan.title, crumbs[2].text_content())
+ self.assertIn(self.action.name, crumbs[3].text_content())
+
def test_plan_action_delete_login_required_http_403(self):
response = self.client.post(self.action_delete_url)
self.assertEquals(response.status_code, 403)
@@ -1000,6 +1015,54 @@ def test_search_returns_expected_plans_when_matched_with_plan_title(self):
)
+class NewPlanContactCounterTest(WebTest):
+ def test_plan_contact_new_count(self):
+ owner = UserFactory()
+ plan_1 = PlanFactory(created_by=owner)
+ plan_2 = PlanFactory(created_by=owner)
+
+ user = UserFactory()
+
+ root_url = reverse("root")
+ list_url = reverse("plans:plan_list")
+
+ # check no number shows by default
+ response = self.app.get(root_url, user=user)
+ links = response.pyquery(
+ f".header__container > .primary-navigation a[href='{list_url}']"
+ )
+ self.assertEqual(len(links), 1)
+ self.assertEqual(links.text(), _("Samenwerken") + " people")
+
+ # check if the number shows up in the menu
+ plan_1.plan_contacts.add(user)
+ plan_2.plan_contacts.add(user)
+ self.assertEqual(2, user.get_plan_contact_new_count())
+
+ response = self.app.get(root_url, user=user)
+ links = response.pyquery(
+ f".header__container > .primary-navigation a[href='{list_url}']"
+ )
+ # second link appears
+ self.assertEqual(len(links), 2)
+ self.assertIn("(2)", links.text())
+
+ # access the list page to reset
+ response = self.app.get(list_url, user=user)
+ links = response.pyquery(
+ f".header__container > .primary-navigation a[href='{list_url}']"
+ )
+ self.assertEqual(len(links), 1)
+ self.assertEqual(links.text(), _("Samenwerken") + " people")
+
+ # check this doesn't appear for owner
+ response = self.app.get(root_url, user=owner)
+ links = response.pyquery(
+ f".header__container > .primary-navigation a[href='{list_url}']"
+ )
+ self.assertEqual(len(links), 1)
+
+
@multi_browser()
class PlanActionStatusPlaywrightTests(ActionsPlaywrightTests):
def setUp(self) -> None:
diff --git a/src/open_inwoner/plans/urls.py b/src/open_inwoner/plans/urls.py
index 2669fe2a5a..7489532090 100644
--- a/src/open_inwoner/plans/urls.py
+++ b/src/open_inwoner/plans/urls.py
@@ -5,6 +5,7 @@
PlanActionDeleteView,
PlanActionEditStatusTagView,
PlanActionEditView,
+ PlanActionHistoryView,
PlanCreateView,
PlanDetailView,
PlanEditView,
@@ -42,5 +43,10 @@
PlanActionDeleteView.as_view(),
name="plan_action_delete",
),
+ path(
+ "/actions//history/",
+ PlanActionHistoryView.as_view(),
+ name="plan_action_history",
+ ),
path("/export/", PlanExportView.as_view(), name="plan_export"),
]
diff --git a/src/open_inwoner/plans/views.py b/src/open_inwoner/plans/views.py
index 968d743851..32e0aeb172 100644
--- a/src/open_inwoner/plans/views.py
+++ b/src/open_inwoner/plans/views.py
@@ -17,6 +17,7 @@
from open_inwoner.accounts.views.actions import (
ActionCreateView,
ActionDeleteView,
+ ActionHistoryView,
ActionUpdateStatusTagView,
ActionUpdateView,
BaseActionFilter,
@@ -95,6 +96,10 @@ def get_queryset(self):
.order_by("end_date")
)
+ def get(self, request, *args, **kwargs):
+ self.request.user.clear_plan_contact_new_count()
+ return super().get(request, *args, **kwargs)
+
def get_available_contacts_for_filtering(self, plans):
"""
Return all available contacts for filtering for all the plans.
@@ -178,7 +183,7 @@ class PlanDetailView(
@cached_property
def crumbs(self):
return [
- (_("Samenwerkingsplannen"), reverse("plans:plan_list")),
+ (_("Samenwerken"), reverse("plans:plan_list")),
(self.get_object().title, reverse("plans:plan_detail", kwargs=self.kwargs)),
]
@@ -223,8 +228,8 @@ class PlanCreateView(
@cached_property
def crumbs(self):
return [
- (_("Samenwerkingsplannen"), reverse("plans:plan_list")),
- (_("Maak samenwerkingsplan aan"), reverse("plans:plan_create")),
+ (_("Samenwerken"), reverse("plans:plan_list")),
+ (_("Start nieuwe samenwerking"), reverse("plans:plan_create")),
]
def get_form_kwargs(self):
@@ -520,6 +525,30 @@ def on_delete_action(self, action):
)
+class PlanActionHistoryView(ActionHistoryView):
+ @cached_property
+ def crumbs(self):
+ return [
+ (_("Samenwerking"), reverse("plans:plan_list")),
+ (
+ self.get_plan().title,
+ reverse("plans:plan_detail", kwargs={"uuid": self.get_plan().uuid}),
+ ),
+ (
+ _("History of {}").format(self.object.name),
+ reverse("plans:plan_action_history", kwargs=self.kwargs),
+ ),
+ ]
+
+ def get_plan(self):
+ try:
+ return Plan.objects.connected(self.request.user).get(
+ uuid=self.kwargs.get("plan_uuid")
+ )
+ except ObjectDoesNotExist as e:
+ raise Http404
+
+
class PlanExportView(
PlansEnabledMixin, LogMixin, LoginRequiredMixin, ExportMixin, DetailView
):
diff --git a/src/open_inwoner/scss/components/Card/Card.scss b/src/open_inwoner/scss/components/Card/Card.scss
index cbd162a6a9..750fad316c 100644
--- a/src/open_inwoner/scss/components/Card/Card.scss
+++ b/src/open_inwoner/scss/components/Card/Card.scss
@@ -73,6 +73,11 @@
}
&__body {
padding: var(--card-spacing);
+
+ /// clickable plans on home page after authenticated
+ .plan-list {
+ text-decoration: none;
+ }
}
&__body--grid {
@@ -159,7 +164,8 @@
}
/// Arrow button on product cards.
- a.button:last-child {
+ a.button:last-child,
+ span.button:last-child {
float: right;
}
diff --git a/src/open_inwoner/scss/components/CardContainer/CardContainer.scss b/src/open_inwoner/scss/components/CardContainer/CardContainer.scss
index 5f556a6f3b..7afed236f1 100644
--- a/src/open_inwoner/scss/components/CardContainer/CardContainer.scss
+++ b/src/open_inwoner/scss/components/CardContainer/CardContainer.scss
@@ -31,3 +31,12 @@
.card-container + h2 {
margin-top: var(--gutter-width);
}
+
+/// Exceptions for forms inside cards
+
+.registration-grid,
+.login-grid {
+ .card {
+ max-width: 100%;
+ }
+}
diff --git a/src/open_inwoner/scss/components/Cases/Cases.scss b/src/open_inwoner/scss/components/Cases/Cases.scss
index 05171f8f2e..08acb5d0ce 100644
--- a/src/open_inwoner/scss/components/Cases/Cases.scss
+++ b/src/open_inwoner/scss/components/Cases/Cases.scss
@@ -1,3 +1,17 @@
.cases {
margin-top: var(--spacing-giant);
+
+ /// cards on cases page
+ .card {
+ .cases__link {
+ text-decoration: none;
+ }
+ }
+}
+
+#document-upload .button[type='submit']:disabled {
+ border-color: var(--color-gray) !important;
+ color: var(--color-gray-light);
+ pointer-events: none;
+ cursor: default;
}
diff --git a/src/open_inwoner/scss/components/Contacts/Contacts.scss b/src/open_inwoner/scss/components/Contacts/Contacts.scss
index 7647e4fa59..727b4b855e 100644
--- a/src/open_inwoner/scss/components/Contacts/Contacts.scss
+++ b/src/open_inwoner/scss/components/Contacts/Contacts.scss
@@ -9,6 +9,9 @@
}
.avatar {
+ display: flex;
+ justify-content: center;
+ align-items: center;
padding: 15px 15px 10px;
margin-top: var(--gutter-width);
border: var(--border-width) solid var(--color-success-lighter);
diff --git a/src/open_inwoner/scss/components/Header/AnchorMenu.scss b/src/open_inwoner/scss/components/Header/AnchorMenu.scss
index 10f69f00ea..a120958ced 100644
--- a/src/open_inwoner/scss/components/Header/AnchorMenu.scss
+++ b/src/open_inwoner/scss/components/Header/AnchorMenu.scss
@@ -17,7 +17,7 @@
width: 100%;
list-style: none;
margin: 0;
- padding: 0;
+ padding: var(--spacing-extra-large);
z-index: 1002;
&--desktop {
@@ -66,11 +66,13 @@
.link {
box-sizing: border-box;
- padding: var(--spacing-large);
+ padding: var(--spacing-medium) var(--spacing-large) var(--spacing-medium)
+ var(--spacing-large);
@media (min-width: 768px) {
border-left: var(--border-width) solid;
border-color: var(--color-gray-light);
+ padding: var(--spacing-large);
}
}
@@ -84,7 +86,8 @@
&--mobile__title {
box-sizing: border-box;
- padding: var(--spacing-large);
+ padding: var(--spacing-large) var(--spacing-large) var(--spacing-medium)
+ var(--spacing-large);
&.h4 {
color: var(--color-primary);
diff --git a/src/open_inwoner/scss/components/List/_List.scss b/src/open_inwoner/scss/components/List/_List.scss
index 3bb15b1468..5af1bfa451 100644
--- a/src/open_inwoner/scss/components/List/_List.scss
+++ b/src/open_inwoner/scss/components/List/_List.scss
@@ -4,6 +4,10 @@
padding: 0;
width: 100%;
overflow-y: auto;
+
+ .case-list {
+ text-decoration: none;
+ }
}
.search + .list {
diff --git a/src/open_inwoner/scss/views/Categories.scss b/src/open_inwoner/scss/views/Categories.scss
new file mode 100644
index 0000000000..90f6391bc1
--- /dev/null
+++ b/src/open_inwoner/scss/views/Categories.scss
@@ -0,0 +1,24 @@
+.categories {
+ &__content {
+ .card-container {
+ grid-template-columns: repeat(auto-fit, 228px);
+
+ .card {
+ max-width: 360px;
+ }
+ }
+ }
+
+ &__products {
+ margin-top: var(--gutter-width);
+
+ .card-container {
+ margin-top: var(--gutter-width);
+ grid-template-columns: repeat(var(--card-columns), 1fr);
+
+ .card {
+ max-width: 100%;
+ }
+ }
+ }
+}
diff --git a/src/open_inwoner/scss/views/Home.scss b/src/open_inwoner/scss/views/Home.scss
new file mode 100644
index 0000000000..4ae6361473
--- /dev/null
+++ b/src/open_inwoner/scss/views/Home.scss
@@ -0,0 +1,11 @@
+/// Cards on Home Page and Theme page
+
+.home {
+ .card-container {
+ grid-template-columns: repeat(auto-fit, 228px);
+
+ .card {
+ max-width: 360px;
+ }
+ }
+}
diff --git a/src/open_inwoner/scss/views/_index.scss b/src/open_inwoner/scss/views/_index.scss
index 51b490f1b6..15b4cba304 100644
--- a/src/open_inwoner/scss/views/_index.scss
+++ b/src/open_inwoner/scss/views/_index.scss
@@ -1,5 +1,7 @@
@import './App.scss';
@import './body';
+@import './Categories.scss';
+@import './Home.scss';
@import './Plans.scss';
@import './product_detail';
@import './view';
diff --git a/src/open_inwoner/templates/pages/cases/list.html b/src/open_inwoner/templates/pages/cases/list.html
index b7cf5bcdd5..2d490fa27b 100644
--- a/src/open_inwoner/templates/pages/cases/list.html
+++ b/src/open_inwoner/templates/pages/cases/list.html
@@ -1,5 +1,5 @@
{% extends 'master.html' %}
-{% load i18n anchor_menu_tags card_tags grid_tags icon_tags link_tags list_tags pagination_tags utils %}
+{% load i18n anchor_menu_tags grid_tags icon_tags link_tags list_tags pagination_tags utils %}
{% block sidebar_content %}
{% anchor_menu anchors=anchors desktop=True %}
@@ -13,15 +13,27 @@ {{ page_title }} ({{ paginator.count }})
{% render_grid %}
{% for case in cases %}
{% render_column start=forloop.counter_0|multiply:4 span=4 %}
- {% render_card compact=True stretch=True title=case.identificatie %}
- {% render_list %}
- {% list_item case.current_status caption=_("Status") compact=True strong=False %}
- {% list_item case.start_date caption=_("Ontvangstdatum") compact=True strong=False %}
- {% list_item case.description caption=_("Omschrijving") compact=True strong=False %}
- {% endrender_list %}
+
{% endrender_column %}
{% endfor %}
{% endrender_grid %}
diff --git a/src/open_inwoner/templates/pages/category/detail.html b/src/open_inwoner/templates/pages/category/detail.html
index d53218cc78..5404a6c932 100644
--- a/src/open_inwoner/templates/pages/category/detail.html
+++ b/src/open_inwoner/templates/pages/category/detail.html
@@ -10,39 +10,43 @@
{% endblock header_image %}
{% block content %}
-
- {{ object.name }}
- {% if request.user.is_staff %}
- {% button icon="edit" text=_("Open in admin") hide_text=True href="admin:pdc_category_change" object_id=object.pk %}
- {% endif %}
-
- {{ object.description|linebreaksbr }}
+
+
+ {{ object.name }}
+ {% if request.user.is_staff %}
+ {% button icon="edit" text=_("Open in admin") hide_text=True href="admin:pdc_category_change" object_id=object.pk %}
+ {% endif %}
+
+
{{ object.description|linebreaksbr }}
- {% if subcategories %}
- {% card_container subcategories=subcategories parent_category=object %}
- {% endif %}
+ {% if subcategories %}
+ {% card_container subcategories=subcategories parent_category=object %}
+ {% endif %}
- {% if products %}
- {% card_container products=products small=True parent=object %}
- {% endif %}
+ {% if products %}
+
+ {% card_container products=products small=True parent=object %}
+
+ {% endif %}
- {% if category.question_set.all %}
- {% render_grid %}
- {% render_column span=6 %}
- {% faq category.question_set.all %}
- {% endrender_column %}
- {% endrender_grid %}
- {% endif %}
+ {% if category.question_set.all %}
+ {% render_grid %}
+ {% render_column span=6 %}
+ {% faq category.question_set.all %}
+ {% endrender_column %}
+ {% endrender_grid %}
+ {% endif %}
- {% if questionnaire_roots %}
-
-
-
+ {% if questionnaire_roots %}
+
+
+
+
-
- {% endif %}
+ {% endif %}
+
{% endblock content %}
diff --git a/src/open_inwoner/templates/pages/category/list.html b/src/open_inwoner/templates/pages/category/list.html
index 212d9422e1..03f6d0f2ef 100644
--- a/src/open_inwoner/templates/pages/category/list.html
+++ b/src/open_inwoner/templates/pages/category/list.html
@@ -2,8 +2,10 @@
{% load card_tags %}
{% block content %}
-
{{configurable_text.theme_page.theme_title}}
-
{{configurable_text.theme_page.theme_intro|linebreaksbr}}
+
+
{{configurable_text.theme_page.theme_title}}
+
{{configurable_text.theme_page.theme_intro|linebreaksbr}}
-{% card_container categories=object_list %}
+ {% card_container categories=object_list %}
+
{% endblock content %}
diff --git a/src/open_inwoner/templates/pages/home.html b/src/open_inwoner/templates/pages/home.html
index 8ab2a73d45..95c30efd82 100644
--- a/src/open_inwoner/templates/pages/home.html
+++ b/src/open_inwoner/templates/pages/home.html
@@ -8,6 +8,7 @@
{% endblock header_image %}
{% block content %}
+
{% block user_content %}
{{configurable_text.home_page.home_welcome_title}}
@@ -24,7 +25,7 @@
{{configurable_text.home_page.home_theme_intro|linebreaksbr}}
{% if request.user.is_authenticated %}
- {% card_container categories=categories columns=3 image_object_fit="cover" %}
+ {% card_container categories=categories columns=4 image_object_fit="cover" %}
{% else %}
{% card_container categories=categories image_object_fit="cover" %}
{% endif %}
@@ -45,4 +46,5 @@
{{configurable_text.home_page.home_map_title}}
{% with centroid=product_locations.get_centroid %}
{% map centroid.lat centroid.lng geojson_feature_collection=product_locations.get_geojson_feature_collection %}
{% endwith %}
+
{% endblock %}
diff --git a/src/open_inwoner/templates/pages/product/detail.html b/src/open_inwoner/templates/pages/product/detail.html
index 72196a4cf1..f76207238e 100644
--- a/src/open_inwoner/templates/pages/product/detail.html
+++ b/src/open_inwoner/templates/pages/product/detail.html
@@ -1,5 +1,5 @@
{% extends 'master.html' %}
-{% load i18n l10n button_tags card_tags faq_tags file_tags grid_tags icon_tags link_tags map_tags notification_tags tag_tags utils condition_tags render_tags anchor_menu_tags %}
+{% load i18n l10n button_tags card_tags faq_tags file_tags grid_tags icon_tags link_tags map_tags notification_tags tag_tags utils condition_tags render_tags anchor_menu_tags product_tags %}
{% block header_image %}
{% if object.image %}
@@ -32,7 +32,7 @@
{% tag tags=object.tags.all %}
{{ object.summary }}
- {{ object.content|ckeditor_content|safe }}
+ {{ object|product_ckeditor_content|safe }}
{% if product.form %}
{% button_row mobile=True %}
@@ -86,10 +86,10 @@
{% trans "U komt niet in aanmerking" %}
{{ location.address_line_1 }}
{{ location.address_line_2 }}
{% if location.phonenumber %}
-
{{ location.phonenumber }}
+ {% link href='tel:{{location.phonenumber}}' primary=True text=location.phonenumber %}
{% endif %}
{% if location.email %}
-
{{ location.email }}
+ {% link href='mailto:{{location.email}}' primary=True text=location.email %}
{% endif %}
{% endrender_card %}
diff --git a/src/open_inwoner/templates/pages/profile/contacts/list.html b/src/open_inwoner/templates/pages/profile/contacts/list.html
index 62182e1519..8892d61fc7 100644
--- a/src/open_inwoner/templates/pages/profile/contacts/list.html
+++ b/src/open_inwoner/templates/pages/profile/contacts/list.html
@@ -1,5 +1,5 @@
{% extends 'master.html' %}
-{% load i18n button_tags link_tags icon_tags pagination_tags form_tags dropdown_tags messages_tags thumbnail %}
+{% load i18n button_tags link_tags icon_tags pagination_tags form_tags dropdown_tags messages_tags thumbnail cropping %}
{% block content %}
@@ -51,10 +51,14 @@
{% trans "U bent toegevoegd als contactpersoon" %}
{% endif %}
- {% if approvals_count == 1 and request.user.image %}
-
-

-
+ {% if approvals_count == 1 %}
+ {% with pending_approvals.get as pending_approval %}
+ {% if pending_approval.image %}
+
+

+
+ {% endif %}
+ {% endwith %}
{% endif %}
{% endwith %}
diff --git a/src/open_inwoner/templates/pages/user-home.html b/src/open_inwoner/templates/pages/user-home.html
index c46d8c5b36..b61a44275e 100644
--- a/src/open_inwoner/templates/pages/user-home.html
+++ b/src/open_inwoner/templates/pages/user-home.html
@@ -14,12 +14,12 @@