Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#4396] Objects API prefill plugin #4566

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/configuration/prefill/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Prefill plugins
kvk
stuf_bg
suwinet
objects_api
3 changes: 3 additions & 0 deletions docs/developers/plugins/prefill_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ You can find an example implementation in :mod:`openforms.prefill.contrib.demo`.
Implementation
--------------

Plugins must be added to the INSTALLED_APPS :mod:`openforms.conf.base`. See the demo app as an example
("openforms.prefill.contrib.demo.apps.DemoApp")

Plugins must implement the interface from :class:`openforms.prefill.base.BasePlugin`.
It's safe to use this as a base class.

Expand Down
1 change: 1 addition & 0 deletions src/openforms/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
"openforms.prefill.contrib.stufbg.apps.StufBgApp",
"openforms.prefill.contrib.haalcentraal_brp.apps.HaalCentraalBRPApp",
"openforms.prefill.contrib.suwinet.apps.SuwinetApp",
"openforms.prefill.contrib.objects_api.apps.ObjectsApiApp",
"openforms.authentication",
"openforms.authentication.contrib.demo.apps.DemoApp",
"openforms.authentication.contrib.outage.apps.DemoOutageApp",
Expand Down
2 changes: 2 additions & 0 deletions src/openforms/forms/admin/form_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class FormVariableAdmin(admin.ModelAdmin):
"prefill_plugin",
"prefill_attribute",
"prefill_identifier_role",
"prefill_options",
"data_type",
"is_sensitive_data",
"initial_value",
Expand All @@ -31,6 +32,7 @@ class FormVariableAdmin(admin.ModelAdmin):
"prefill_plugin",
"prefill_attribute",
"prefill_identifier_role",
"prefill_options",
"data_type",
"data_format",
"is_sensitive_data",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.15 on 2024-08-21 11:04

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("forms", "0097_v267_to_v270"),
]

operations = [
migrations.AddField(
model_name="formvariable",
name="prefill_options",
field=models.JSONField(
blank=True, default=dict, verbose_name="prefill options"
),
),
]
5 changes: 5 additions & 0 deletions src/openforms/forms/models/form_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ class FormVariable(models.Model):
default=IdentifierRoles.main,
max_length=100,
)
prefill_options = models.JSONField(
_("prefill options"),
default=dict,
blank=True,
)
data_type = models.CharField(
verbose_name=_("data type"),
help_text=_("The type of the value that will be associated with this variable"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,10 @@ const AttributeField = () => {
} = useAsync(async () => {
if (!plugin) return [];

const endpoint = `/api/v2/prefill/plugins/${plugin}/attributes`;
// const endpoint = `/api/v2/prefill/plugins/${plugin}/attributes`;
const endpoint = `/api/v2/prefill/plugins/objects-api/objecttypes/ac1fa3f8-fb2a-4fcb-b715-d480aceeda10/versions/1/attributes`;
// XXX: clean up error handling here at some point...
const response = await get(endpoint);
const response = await get(endpoint, {objects_api_group: '2'});
if (!response.ok) throw response.data;
return response.data.map(attribute => [attribute.id, attribute.label]);
}, [plugin]);
Expand Down
5 changes: 4 additions & 1 deletion src/openforms/js/components/formio_builder/WebformBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {currentTheme} from 'utils/theme';

import {
getPrefillAttributes,
getPrefillObjectsAPIGroups,
getPrefillObjectsAPIObjecttypeVersions,
getPrefillObjectsAPIObjecttypes,
getPrefillPlugins,
getRegistrationAttributes,
getValidatorPlugins,
Expand Down Expand Up @@ -166,7 +169,7 @@ class WebformBuilder extends WebformBuilderFormio {
getValidatorPlugins={getValidatorPlugins}
getRegistrationAttributes={getRegistrationAttributes}
getPrefillPlugins={getPrefillPlugins}
getPrefillAttributes={getPrefillAttributes}
getPrefillAttributes={getPrefillObjectsAPIObjecttypeVersions}
getFileTypes={async () => FILE_TYPES}
serverUploadLimit={MAX_FILE_UPLOAD_SIZE}
getDocumentTypes={async () => await getAvailableDocumentTypes(this)}
Expand Down
32 changes: 30 additions & 2 deletions src/openforms/js/components/formio_builder/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,35 @@ export const getPrefillPlugins = async () => {
return resp.data;
};

export const getPrefillAttributes = async plugin => {
const resp = await get(`/api/v2/prefill/plugins/${plugin}/attributes`);
// export const getPrefillAttributes = async plugin => {
// // const resp = await get(`/api/v2/prefill/plugins/${plugin}/attributes`);
// const resp = await get(`/api/v2/prefill/plugins/objects-api/groups`);
// return resp.data;
// };

// export const getPrefillObjectsAPIGroups = async () => {
// const resp = await get(`/api/v2/prefill/plugins/objects-api/groups`);
// return resp.data;
// };

// export const getPrefillObjectsAPIObjecttypes = async () => {
// const resp = await get(`/api/v2/prefill/plugins/objects-api/objecttypes/2`);
// return resp.data;
// };

// export const getPrefillObjectsAPIObjecttypeVersions = async () => {
// const resp = await get(
// `/api/v2/prefill/plugins/objects-api/objecttypes/2/ac1fa3f8-fb2a-4fcb-b715-d480aceeda10/versions`
// );
// return resp.data;
// };

export const getPrefillObjectsAPIObjecttypeVersions = async () => {
const resp = await get(
`/api/v2/prefill/plugins/objects-api/objecttypes/ac1fa3f8-fb2a-4fcb-b715-d480aceeda10/versions/1/attributes`,
{
objects_api_group: '2',
}
);
return resp.data;
};
33 changes: 33 additions & 0 deletions src/openforms/prefill/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,36 @@ class PrefillAttributeSerializer(serializers.Serializer):
label=_("Label"),
help_text=_("The human-readable name for an attribute."),
)


class PrefillObjectsAPIObjecttypeSerializer(serializers.Serializer):
# Keys are defined in camel case as this is what we get from the Objecttype API
url = serializers.URLField(
label=_(
"URL reference to this object. This is the unique identification and location of this object."
),
)
uuid = serializers.UUIDField(label=_("Unique identifier (UUID4)."))
name = serializers.CharField(label=_("Name of the object type."))
namePlural = serializers.CharField(label=_("Plural name of the object type."))
dataClassification = serializers.CharField(
label=_("Confidential level of the object type.")
)


class PrefillObjectsAPIObjecttypeVersionSerializer(serializers.Serializer):
version = serializers.IntegerField(
label=_("Integer version of the Objecttype."),
)
status = serializers.CharField(label=_("Status of the object type version"))


class PrefillObjectsAPIAttributeSerializer(serializers.Serializer):
id = serializers.CharField(
label=_("ID"),
help_text=_("The unique attribute identifier"),
)
label = serializers.CharField(
label=_("Label"),
help_text=_("The human-readable name for an attribute."),
)
24 changes: 23 additions & 1 deletion src/openforms/prefill/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
from django.urls import path

from .views import PluginAttributesListView, PluginListView
from .views import (
PluginAttributesListView,
PluginListView,
PluginObjectsAPIAttributesListView,
PluginObjectsAPIObjecttypeListView,
PluginObjectsAPIObjecttypeVersionListView,
)

urlpatterns = [
path(
# "plugins/objects-api/objecttypes/<str:objects_api_group>",
"plugins/objects-api/objecttypes",
PluginObjectsAPIObjecttypeListView.as_view(),
name="prefill-objects-api-objecttype-list",
),
path(
"plugins/objects-api/objecttypes/<uuid:objects_api_objecttype_uuid>/versions",
PluginObjectsAPIObjecttypeVersionListView.as_view(),
name="prefill-objects-api-objecttype-version-list",
),
path(
"plugins/objects-api/objecttypes/<uuid:objects_api_objecttype_uuid>/versions/<int:objects_api_objecttype_version>/attributes",
PluginObjectsAPIAttributesListView.as_view(),
name="prefill-objects-api-objecttype-attribute-list",
),
path("plugins", PluginListView.as_view(), name="prefill-plugin-list"),
path(
"plugins/<slug:plugin>/attributes",
Expand Down
106 changes: 105 additions & 1 deletion src/openforms/prefill/api/views.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
from typing import Any

from django.utils.translation import gettext_lazy as _

from drf_spectacular.utils import extend_schema, extend_schema_view
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
from rest_framework import authentication, permissions
from rest_framework.exceptions import NotFound
from rest_framework.views import APIView

from openforms.api.views import ListMixin
from openforms.registrations.contrib.objects_api.api.serializers import (
ObjectsAPIGroupInputSerializer,
)
from openforms.registrations.contrib.objects_api.client import get_objecttypes_client

from ..registry import register
from .serializers import (
ChoiceWrapper,
PrefillAttributeSerializer,
PrefillObjectsAPIAttributeSerializer,
PrefillObjectsAPIObjecttypeSerializer,
PrefillObjectsAPIObjecttypeVersionSerializer,
PrefillPluginQueryParameterSerializer,
PrefillPluginSerializer,
)

OBJECTS_API_GROUP_QUERY_PARAMETER = OpenApiParameter(
name="objects_api_group",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description=_("Which Objects API group to use."),
)


@extend_schema_view(
get=extend_schema(
Expand Down Expand Up @@ -67,3 +84,90 @@ def get_objects(self):
choices = plugin.get_available_attributes()

return [ChoiceWrapper(choice) for choice in choices]


@extend_schema_view(
get=extend_schema(summary=_("List available objecttypes for Objects API")),
parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER],
)
class PluginObjectsAPIObjecttypeListView(ListMixin, APIView):
"""
List the available prefill objecttypes for Objects API plugin.
"""

authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAdminUser,)
serializer_class = PrefillObjectsAPIObjecttypeSerializer

def get_objects(self) -> list[dict[str, Any]]:
input_serializer = ObjectsAPIGroupInputSerializer(
data=self.request.query_params
)
input_serializer.is_valid(raise_exception=True)

config_group = input_serializer.validated_data["objects_api_group"]

with get_objecttypes_client(config_group) as client:
return client.list_objecttypes()


@extend_schema_view(
get=extend_schema(summary=_("List available objecttype versions for Objects API")),
parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER],
)
class PluginObjectsAPIObjecttypeVersionListView(ListMixin, APIView):
Comment on lines +93 to +118
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these views do already seem to exist for registrations, so we might be able to use those instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I can use them as the base class and have the new one here for the different urls as well. I guess you mean I have to do the same for the serializers since some of them are the same with the ones in registrations.

"""
List the available prefill objecttype versions for Objects API plugin.
"""

authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAdminUser,)
serializer_class = PrefillObjectsAPIObjecttypeVersionSerializer

def get_objects(self):
input_serializer = ObjectsAPIGroupInputSerializer(
data=self.request.query_params
)
input_serializer.is_valid(raise_exception=True)

config_group = input_serializer.validated_data["objects_api_group"]
objecttype_uuid = self.kwargs["objects_api_objecttype_uuid"]

with get_objecttypes_client(config_group) as client:
return client.list_objecttype_versions(objecttype_uuid)


@extend_schema_view(
get=extend_schema(summary=_("List available attributes for Objects API")),
parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER],
)
class PluginObjectsAPIAttributesListView(ListMixin, APIView):
"""
List the available attributes for Objects API plugin.
"""

authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAdminUser,)
serializer_class = PrefillObjectsAPIAttributeSerializer

def get_objects(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this method should return a list of tuples with each a label and value (e.g. choices)?

EDIT: I just saw parse_schema_properties and that returns a list of tuples, so I think that's right

plugin = register["objects_api"]
input_serializer = ObjectsAPIGroupInputSerializer(
data=self.request.query_params
)
input_serializer.is_valid(raise_exception=True)

config_group = input_serializer.validated_data["objects_api_group"]
choices = plugin.get_available_attributes(
reference={
"objects_api_group": config_group,
"objects_api_objecttype_uuid": self.kwargs[
"objects_api_objecttype_uuid"
],
"objects_api_objecttype_version": self.kwargs[
"objects_api_objecttype_version"
],
}
)

return choices
7 changes: 6 additions & 1 deletion src/openforms/prefill/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ class BasePlugin(AbstractBasePlugin):
for_components: Container[str] = AllComponentTypes()

@staticmethod
def get_available_attributes() -> Iterable[tuple[str, str]]:
def get_available_attributes(
reference: dict[str, str] | None = None,
) -> Iterable[tuple[str, str]]:
"""
Return a choice list of available attributes this plugin offers.

:param reference: a dict based on which we retrieve the available attributes.
Can be used when we have dynamic lists of attributes.
"""
raise NotImplementedError(
"You must implement the 'get_available_attributes' method."
Expand Down
3 changes: 3 additions & 0 deletions src/openforms/prefill/contrib/objects_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Objects API prefill plugin.
"""
12 changes: 12 additions & 0 deletions src/openforms/prefill/contrib/objects_api/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class ObjectsApiApp(AppConfig):
name = "openforms.prefill.contrib.objects_api"
label = "objects_api"
verbose_name = _("Objects API prefill plugin")

def ready(self):
# register the plugin
from . import plugin # noqa
19 changes: 19 additions & 0 deletions src/openforms/prefill/contrib/objects_api/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.db import models
from django.utils.translation import gettext_lazy as _


class ObjectsAPIAttributes(models.TextChoices):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we'll need this, this was probably from your initial changes that you made before? @vaszig

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, probably not

url = "url", _("Url")
uuid = "uuid", _("UUID")
type = "type", _("Type")
record_index = "record.index", _("Record > Index")
record_typeVersion = "record.typeVersion", _("Record > Type version")

record_data_ = "", _("Record > Data")

record_geometry = "record.geometry", _("Record > Geometry")
record_startAt = "record.startAt", _("Record > Start at")
record_endAt = "record.endAt", _("Record > End at")
record_registrationAt = "record.registrationAt", _("Record > Registration at")
record_correctionFor = "record.correctionFor", _("Record > Correction for")
record_correctedBy = "record_correctedBy", _("Record > Corrected by")
Loading
Loading