From 49a432f48f2f868d733b9bc37cef7a6b315aef62 Mon Sep 17 00:00:00 2001 From: Alex de Landgraaf Date: Wed, 27 Sep 2023 17:39:24 +0200 Subject: [PATCH 1/2] Adding Objects API client integration --- src/open_inwoner/cms/objects/__init__.py | 0 src/open_inwoner/cms/objects/cms_plugins.py | 14 ++++++ .../cms/objects/migrations/0001_initial.py | 46 +++++++++++++++++++ .../cms/objects/migrations/__init__.py | 0 src/open_inwoner/cms/objects/models.py | 20 ++++++++ src/open_inwoner/conf/base.py | 3 ++ .../templates/cms/objects/objects_list.html | 17 +++++++ 7 files changed, 100 insertions(+) create mode 100644 src/open_inwoner/cms/objects/__init__.py create mode 100644 src/open_inwoner/cms/objects/cms_plugins.py create mode 100644 src/open_inwoner/cms/objects/migrations/0001_initial.py create mode 100644 src/open_inwoner/cms/objects/migrations/__init__.py create mode 100644 src/open_inwoner/cms/objects/models.py create mode 100644 src/open_inwoner/templates/cms/objects/objects_list.html diff --git a/src/open_inwoner/cms/objects/__init__.py b/src/open_inwoner/cms/objects/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/open_inwoner/cms/objects/cms_plugins.py b/src/open_inwoner/cms/objects/cms_plugins.py new file mode 100644 index 0000000000..2fc06260cb --- /dev/null +++ b/src/open_inwoner/cms/objects/cms_plugins.py @@ -0,0 +1,14 @@ +from django.utils.translation import ugettext_lazy as _ + +from cms.plugin_base import CMSPluginBase +from cms.plugin_pool import plugin_pool + +from .models import ObjectsList + + +@plugin_pool.register_plugin +class ObjectsListPlugin(CMSPluginBase): + model = ObjectsList + name = _("Object List Plugin") + render_template = "cms/objects/objects_list.html" + cache = False diff --git a/src/open_inwoner/cms/objects/migrations/0001_initial.py b/src/open_inwoner/cms/objects/migrations/0001_initial.py new file mode 100644 index 0000000000..b57d04630d --- /dev/null +++ b/src/open_inwoner/cms/objects/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.20 on 2023-09-17 20:14 + +import django.db.models.deletion +from django.db import migrations, models + +import objectsapiclient.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("cms", "0022_auto_20180620_1551"), + ] + + operations = [ + migrations.CreateModel( + name="ObjectsList", + fields=[ + ( + "cmsplugin_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="objects_objectslist", + serialize=False, + to="cms.cmsplugin", + ), + ), + ("title", models.CharField(max_length=250, verbose_name="Title")), + ( + "object_type", + objectsapiclient.models.ObjectTypeField( + db_index=False, max_length=100 + ), + ), + ], + options={ + "abstract": False, + }, + bases=("cms.cmsplugin",), + ), + ] diff --git a/src/open_inwoner/cms/objects/migrations/__init__.py b/src/open_inwoner/cms/objects/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/open_inwoner/cms/objects/models.py b/src/open_inwoner/cms/objects/models.py new file mode 100644 index 0000000000..d2da14c207 --- /dev/null +++ b/src/open_inwoner/cms/objects/models.py @@ -0,0 +1,20 @@ +import os + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from cms.models import CMSPlugin +from objectsapiclient.models import Configuration, ObjectTypeField + + +class ObjectsList(CMSPlugin): + title = models.CharField(_("Title"), max_length=250) + object_type = ObjectTypeField() # Stores the UUID of the selected object_type + + def get_objects(self): + return Configuration.get_solo().client.get_objects( + object_type_uuid=self.object_type + ) + + def __str__(self): + return self.title diff --git a/src/open_inwoner/conf/base.py b/src/open_inwoner/conf/base.py index 17fadf7c0c..9d63d6aee9 100644 --- a/src/open_inwoner/conf/base.py +++ b/src/open_inwoner/conf/base.py @@ -189,6 +189,7 @@ "mozilla_django_oidc_db", "sessionprofile", "openformsclient", + "objectsapiclient", "django_htmx", "django_yubin", "log_outgoing_requests", @@ -220,6 +221,7 @@ "open_inwoner.cms.footer", "open_inwoner.cms.plugins", "open_inwoner.cms.benefits", + "open_inwoner.cms.objects", ] MIDDLEWARE = [ @@ -542,6 +544,7 @@ "QuestionnairePlugin", "ProductFinderPlugin", "ProductLocationPlugin", + "ObjectsListPlugin", ], "text_only_plugins": ["LinkPlugin"], "name": _("Content"), diff --git a/src/open_inwoner/templates/cms/objects/objects_list.html b/src/open_inwoner/templates/cms/objects/objects_list.html new file mode 100644 index 0000000000..6e34aa1c31 --- /dev/null +++ b/src/open_inwoner/templates/cms/objects/objects_list.html @@ -0,0 +1,17 @@ +{% load i18n sekizai_tags %} + +{% load i18n button_tags card_tags utils icon_tags %} +

+ {{ instance.title }} +

+
+ {% for object in instance.get_objects %} + {% render_card image_object_fit="cover" %} +
+

{{ object.record.index }}

+ {{ object.uuid }} +
+ {% endrender_card %} + {% endfor %} +
+ From 9ab967676aa471c0ddef694b2ce947a12aa8a159 Mon Sep 17 00:00:00 2001 From: bart-maykin Date: Fri, 6 Oct 2023 10:46:24 +0200 Subject: [PATCH 2/2] :sparkles: [#1776] added task components --- src/open_inwoner/cms/objects/cms_plugins.py | 27 +++- src/open_inwoner/cms/objects/constants.py | 20 +++ .../migrations/0002_auto_20231006_1047.py | 131 ++++++++++++++++++ src/open_inwoner/cms/objects/models.py | 129 ++++++++++++++++- .../DenhaagAction/DenhaagAction.html | 15 ++ .../templatetags/denhaag_action_tags.py | 11 ++ src/open_inwoner/js/components/map/index.js | 44 ++++-- .../DenhaagAction/DenhaagAction.scss | 113 +++++++++++++++ src/open_inwoner/scss/components/_index.scss | 1 + .../templates/cms/objects/objects_list.html | 19 ++- .../templates/cms/objects/objects_map.html | 13 ++ src/open_inwoner/utils/templatetags/utils.py | 2 - 12 files changed, 499 insertions(+), 26 deletions(-) create mode 100644 src/open_inwoner/cms/objects/constants.py create mode 100644 src/open_inwoner/cms/objects/migrations/0002_auto_20231006_1047.py create mode 100644 src/open_inwoner/components/templates/components/DenhaagAction/DenhaagAction.html create mode 100644 src/open_inwoner/components/templatetags/denhaag_action_tags.py create mode 100644 src/open_inwoner/scss/components/DenhaagAction/DenhaagAction.scss create mode 100644 src/open_inwoner/templates/cms/objects/objects_map.html diff --git a/src/open_inwoner/cms/objects/cms_plugins.py b/src/open_inwoner/cms/objects/cms_plugins.py index 2fc06260cb..f62bdb2748 100644 --- a/src/open_inwoner/cms/objects/cms_plugins.py +++ b/src/open_inwoner/cms/objects/cms_plugins.py @@ -3,7 +3,7 @@ from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool -from .models import ObjectsList +from .models import ComponentChoices, ObjectsList @plugin_pool.register_plugin @@ -12,3 +12,28 @@ class ObjectsListPlugin(CMSPluginBase): name = _("Object List Plugin") render_template = "cms/objects/objects_list.html" cache = False + + def _get_render_template(self, context, instance, placeholder): + default_template = super()._get_render_template(context, instance, placeholder) + if component := instance.component: + match component: + case ComponentChoices.link: + return "cms/objects/objects_list.html" + case ComponentChoices.map: + return "cms/objects/objects_map.html" + + return default_template + + def render(self, context, instance, placeholder): + request = context["request"] + context["instance"] = instance + context["objects"] = [] + + if request.user.is_authenticated and (bsn := request.user.bsn): + objects = instance.get_objects_by_bsn(bsn) + match instance.component: + case ComponentChoices.link: + context["objects"] = instance.convert_objects_to_actiondata(objects) + case ComponentChoices.map: + context["objects"] = instance.convert_objects_to_geodata(objects) + return context diff --git a/src/open_inwoner/cms/objects/constants.py b/src/open_inwoner/cms/objects/constants.py new file mode 100644 index 0000000000..7f0fe7c840 --- /dev/null +++ b/src/open_inwoner/cms/objects/constants.py @@ -0,0 +1,20 @@ +from django.utils.translation import ugettext_lazy as _ + +from djchoices import ChoiceItem, DjangoChoices + + +class StatusChoices(DjangoChoices): + open = ChoiceItem("open", _("Open")) + submitted = ChoiceItem("ingediend", _("Submitted")) + processed = ChoiceItem("verwerkt", _("Processed")) + closed = ChoiceItem("gesloten", _("Closed")) + + +class DateOrderChoices(DjangoChoices): + ascend = ChoiceItem("+", _("Ascend")) + descent = ChoiceItem("-", _("Descent")) + + +class ComponentChoices(DjangoChoices): + link = ChoiceItem("link", _("Link")) + map = ChoiceItem("map", _("Map")) diff --git a/src/open_inwoner/cms/objects/migrations/0002_auto_20231006_1047.py b/src/open_inwoner/cms/objects/migrations/0002_auto_20231006_1047.py new file mode 100644 index 0000000000..c6c27c42fe --- /dev/null +++ b/src/open_inwoner/cms/objects/migrations/0002_auto_20231006_1047.py @@ -0,0 +1,131 @@ +# Generated by Django 3.2.20 on 2023-10-06 08:47 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("objects", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="objectslist", + name="bsn_path", + field=models.CharField( + blank=True, + help_text="The path to the bsn value from data, for example: 'identificatie.value'", + max_length=250, + verbose_name="BSN Path", + ), + ), + migrations.AddField( + model_name="objectslist", + name="component", + field=models.CharField( + choices=[("link", "Link"), ("map", "Map")], + default="link", + max_length=30, + verbose_name="Component", + ), + ), + migrations.AddField( + model_name="objectslist", + name="date_order", + field=models.CharField( + choices=[("+", "Ascend"), ("-", "Descent")], + default="-", + help_text="Order date on acending or decending.", + max_length=1, + verbose_name="Date Order", + ), + ), + migrations.AddField( + model_name="objectslist", + name="map_lat", + field=models.DecimalField( + decimal_places=6, + default=52.1326, + max_digits=9, + verbose_name="Map starting Latitude", + ), + ), + migrations.AddField( + model_name="objectslist", + name="map_long", + field=models.DecimalField( + decimal_places=6, + default=5.2913, + max_digits=9, + verbose_name="Map starting Longitude", + ), + ), + migrations.AddField( + model_name="objectslist", + name="map_zoom_level", + field=models.IntegerField( + default=3, + validators=[ + django.core.validators.MaxValueValidator(18), + django.core.validators.MinValueValidator(1), + ], + verbose_name="Map starting Longitude", + ), + ), + migrations.AddField( + model_name="objectslist", + name="no_results_message", + field=models.TextField( + default="no results found", + help_text="Text message to tell the user that there are no results found", + verbose_name="No results message", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="objectslist", + name="object_date", + field=models.CharField( + default="start_at", + help_text="The path to the date, for example: 'start_at'", + max_length=250, + verbose_name="Object Date", + ), + ), + migrations.AddField( + model_name="objectslist", + name="object_link", + field=models.CharField( + default="data.formulier.value", + help_text="The path to the url, for example: 'data.formulier.value'", + max_length=250, + verbose_name="Object Link", + ), + ), + migrations.AddField( + model_name="objectslist", + name="object_title", + field=models.CharField( + default="data.title", + help_text="The path to the title, for example: 'data.title'", + max_length=250, + verbose_name="Object Title", + ), + ), + migrations.AddField( + model_name="objectslist", + name="status", + field=models.CharField( + blank=True, + choices=[ + ("open", "Open"), + ("ingediend", "Submitted"), + ("verwerkt", "Processed"), + ("gesloten", "Closed"), + ], + max_length=250, + verbose_name="Status", + ), + ), + ] diff --git a/src/open_inwoner/cms/objects/models.py b/src/open_inwoner/cms/objects/models.py index d2da14c207..b578786c32 100644 --- a/src/open_inwoner/cms/objects/models.py +++ b/src/open_inwoner/cms/objects/models.py @@ -1,20 +1,147 @@ -import os +import json +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.utils.translation import ugettext_lazy as _ from cms.models import CMSPlugin +from glom import glom +from glom.core import PathAccessError, PathAssignError from objectsapiclient.models import Configuration, ObjectTypeField +from .constants import ComponentChoices, DateOrderChoices, StatusChoices + class ObjectsList(CMSPlugin): title = models.CharField(_("Title"), max_length=250) object_type = ObjectTypeField() # Stores the UUID of the selected object_type + status = models.CharField( + _("Status"), + max_length=250, + choices=StatusChoices.choices, + blank=True, + ) + component = models.CharField( + _("Component"), + max_length=30, + choices=ComponentChoices.choices, + default=ComponentChoices.link, + ) + no_results_message = models.TextField( + _("No results message"), + help_text=_("Text message to tell the user that there are no results found"), + ) + date_order = models.CharField( + _("Date Order"), + help_text=_("Order date on acending or decending."), + max_length=1, + choices=DateOrderChoices.choices, + default=DateOrderChoices.descent, + ) + map_lat = models.DecimalField( + _("Map starting Latitude"), max_digits=9, decimal_places=6, default=52.1326 + ) + map_long = models.DecimalField( + _("Map starting Longitude"), max_digits=9, decimal_places=6, default=5.2913 + ) + map_zoom_level = models.IntegerField( + _("Map starting Longitude"), + validators=[MaxValueValidator(18), MinValueValidator(1)], + default=3, + ) + bsn_path = models.CharField( + _("BSN Path"), + max_length=250, + help_text=_( + "The path to the bsn value from data, for example: 'identificatie.value'" + ), + blank=True, + ) + object_title = models.CharField( + _("Object Title"), + max_length=250, + default="data.title", + help_text=_("The path to the title, for example: 'data.title'"), + ) + object_date = models.CharField( + _("Object Date"), + max_length=250, + default="start_at", + help_text=_("The path to the date, for example: 'start_at'"), + ) + object_link = models.CharField( + _("Object Link"), + max_length=250, + default="data.formulier.value", + help_text=_("The path to the url, for example: 'data.formulier.value'"), + ) + + def _return_self(self): + return f"{self.title}, {self.object_type}, {self.bsn_path}" def get_objects(self): return Configuration.get_solo().client.get_objects( object_type_uuid=self.object_type ) + def get_objects_by_bsn(self, bsn): + data_attrs = [] + if self.object_type and self.bsn_path: + data_attrs.append( + "__".join(self.bsn_path.split(".")) + "__exact__" + str(bsn) + ) + if self.status: + data_attrs.append("status__exact__" + self.status) + + return Configuration.get_solo().client.get_objects_by_bsn( + object_type_uuid=self.object_type, + ordering=self.date_order + "record__startAt", + data_attrs=",".join(data_attrs), + ) + + def convert_objects_to_actiondata(self, objects): + object_list = [] + if objects: + for object in objects: + try: + object_list.append( + { + "title": glom(object.record, self.object_title), + "date": glom(object.record, self.object_date), + "link": glom(object.record, self.object_link), + } + ) + except (PathAccessError, PathAssignError): + pass + + return object_list + + def convert_objects_to_geodata(self, objects): + features = [] + if objects: + for object in objects: + if geometry := object.record.get("geometry"): + try: + features.append( + { + "type": "Feature", + "geometry": geometry, + "properties": { + "name": glom(object.record, self.object_title), + "date": glom(object.record, self.object_date), + "link": glom(object.record, self.object_link), + }, + } + ) + except (PathAccessError, PathAssignError): + pass + + return json.dumps( + { + "type": "FeatureCollection", + "features": features, + } + ) + def __str__(self): return self.title diff --git a/src/open_inwoner/components/templates/components/DenhaagAction/DenhaagAction.html b/src/open_inwoner/components/templates/components/DenhaagAction/DenhaagAction.html new file mode 100644 index 0000000000..bd5d5972d3 --- /dev/null +++ b/src/open_inwoner/components/templates/components/DenhaagAction/DenhaagAction.html @@ -0,0 +1,15 @@ +{% load l10n i18n %} + + +
{{title}}
+
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/src/open_inwoner/components/templatetags/denhaag_action_tags.py b/src/open_inwoner/components/templatetags/denhaag_action_tags.py new file mode 100644 index 0000000000..11a68535e7 --- /dev/null +++ b/src/open_inwoner/components/templatetags/denhaag_action_tags.py @@ -0,0 +1,11 @@ +from django import template + +register = template.Library() + + +@register.inclusion_tag("components/DenhaagAction/DenhaagAction.html") +def denhaag_action(title, date, link, **kwargs): + kwargs["title"] = title + kwargs["date"] = date + kwargs["link"] = link + return kwargs diff --git a/src/open_inwoner/js/components/map/index.js b/src/open_inwoner/js/components/map/index.js index c6be8d11b2..2405ca1b88 100644 --- a/src/open_inwoner/js/components/map/index.js +++ b/src/open_inwoner/js/components/map/index.js @@ -94,6 +94,8 @@ class Map { address_line_2, phonenumber, email, + date, + link, ...properties } = feature.properties @@ -103,6 +105,8 @@ class Map { const displayAddress2 = escapeVariableText(address_line_2) const displayPhonenumber = escapeVariableText(phonenumber) const displayEmail = escapeVariableText(email) + const displayDate = escapeVariableText(date) + const buttonLink = escapeVariableText(link) let title = '' if (locationDetailView) { @@ -115,23 +119,39 @@ class Map { title = displayName } - return ` + const phonenumberElement = ` + + ${displayPhonenumber} + + ` + const emailElement = ` + + ${displayEmail} + + ` + const buttonLinkElement = ` + + Action + + ` + + return ( + `

${title}

-
-

${displayAddress1}

-

${displayAddress2}

- - ${displayPhonenumber} - - - ${displayEmail} - -
- ` + ` + + '
' + + (displayAddress1 ? `

${displayAddress1}

` : '') + + (displayAddress2 ? `

${displayAddress2}

` : '') + + (displayDate ? `

${displayDate}

` : '') + + (displayPhonenumber ? phonenumberElement : '') + + (displayEmail ? emailElement : '') + + (buttonLink ? buttonLinkElement : '') + + '
' + ) } } diff --git a/src/open_inwoner/scss/components/DenhaagAction/DenhaagAction.scss b/src/open_inwoner/scss/components/DenhaagAction/DenhaagAction.scss new file mode 100644 index 0000000000..57d649a687 --- /dev/null +++ b/src/open_inwoner/scss/components/DenhaagAction/DenhaagAction.scss @@ -0,0 +1,113 @@ +.denhaag-action { + display: flex; + justify-content: space-between; + flex-direction: column; + gap: 0.5rem; + color: hsla(0, 0%, 18%, 1); + background-color: hsl(0 0% 100%); + padding-inline-start: 1rem; + padding-inline-end: 1rem; + padding-block-start: 1rem; + padding-block-end: 1rem; + border-style: solid; + border-width: 1px; + border-color: hsl(0 0% 82%); + + @media (width >= 768px) { + flex-direction: row; + align-items: center; + gap: 1.5rem; + padding-block-start: 0.8rem; + padding-block-end: 0.8rem; + } + + &__details { + display: flex; + flex-shrink: 0; + flex-direction: column; + gap: 1rem; + + @media (width >= 768px) { + flex-direction: row; + align-items: center; + gap: 1.5rem; + } + } + + &__content { + font-weight: 600; + } + + &__date { + display: flex; + align-items: center; + gap: 0.8rem; + color: hsl(0 0% 29%); + font-weight: 500 &--warning { + display: flex; + align-items: center; + gap: 0.5rem; + color: hsl(23 100% 27%); + font-weight: 600; + } + } + + &__actions { + display: flex; + align-items: center; + gap: 0.5rem; + + & > a { + width: 100%; + + @media (width >= 768px) { + width: auto; + } + } + } + + &__link-icon { + flex-shrink: 0; + width: 20px; + color: hsl(207 80% 35%); + } + + &__warning-icon { + flex-shrink: 0; + width: 20px; + color: hsl(23 99% 44%); + } + + &--single { + padding-inline-start: 1rem; + padding-inline-end: 1rem; + padding-block-start: 1rem; + padding-block-end: 1rem; + border-top: none; + border-left: none; + border-right: none; + text-decoration: none; + + &:hover, + &.denhaag-action--hover { + background-color: hsl(0 0% 95%); + } + + &:focus, + &.denhaag-action--focus { + outline-color: hsl(47 100% 25%); + outline-width: 2px; + outline-style: dashed; + } + + .denhaag-action__details { + flex-direction: row; + justify-content: space-between; + gap: 0.5rem; + + @media (width >= 768px) { + gap: 0.8rem; + } + } + } +} diff --git a/src/open_inwoner/scss/components/_index.scss b/src/open_inwoner/scss/components/_index.scss index 11966c4d66..c93894e3a7 100644 --- a/src/open_inwoner/scss/components/_index.scss +++ b/src/open_inwoner/scss/components/_index.scss @@ -14,6 +14,7 @@ @import './Container/Container.scss'; @import './CookieConsent/CookieBanner.scss'; @import './Dashboard/Dashboard.scss'; +@import './DenhaagAction/DenhaagAction.scss'; @import './Divider/Divider.scss'; @import './Dropdown/Dropdown.scss'; @import './Faq/Faq.scss'; diff --git a/src/open_inwoner/templates/cms/objects/objects_list.html b/src/open_inwoner/templates/cms/objects/objects_list.html index 6e34aa1c31..0c91183593 100644 --- a/src/open_inwoner/templates/cms/objects/objects_list.html +++ b/src/open_inwoner/templates/cms/objects/objects_list.html @@ -1,17 +1,16 @@ -{% load i18n sekizai_tags %} +{% load i18n sekizai_tags utils denhaag_action_tags %} -{% load i18n button_tags card_tags utils icon_tags %}

{{ instance.title }}

-
- {% for object in instance.get_objects %} - {% render_card image_object_fit="cover" %} -
-

{{ object.record.index }}

- {{ object.uuid }} -
- {% endrender_card %} + +
+ {% if objects %} + {% for object in objects %} + {% denhaag_action title=object.title date=object.date link=object.link %} {% endfor %} + {% else %} +

{{ instance.no_results_message }}

+ {% endif %}
diff --git a/src/open_inwoner/templates/cms/objects/objects_map.html b/src/open_inwoner/templates/cms/objects/objects_map.html new file mode 100644 index 0000000000..c2bcb1345c --- /dev/null +++ b/src/open_inwoner/templates/cms/objects/objects_map.html @@ -0,0 +1,13 @@ +{% load i18n sekizai_tags utils map_tags %} + +

+ {{ instance.title }} +

+
+ {% if objects %} + {% map instance.map_lat instance.map_long zoom=instance.map_zoom_level geojson_feature_collection=objects %} + {% else %} +

{{ instance.no_results_message }}

+ {% endif %} +
+ diff --git a/src/open_inwoner/utils/templatetags/utils.py b/src/open_inwoner/utils/templatetags/utils.py index 832d8ab7a9..1154b42ace 100644 --- a/src/open_inwoner/utils/templatetags/utils.py +++ b/src/open_inwoner/utils/templatetags/utils.py @@ -5,8 +5,6 @@ from humanfriendly import format_size -from open_inwoner.configurations.models import SiteConfiguration - register = template.Library()