Skip to content

Commit

Permalink
✨ [#4606] Implement API endpoint to retrieve role types
Browse files Browse the repository at this point in the history
Given a particular catalogue and case type identification, retrieve the
role types defined within.

We exclude the initiator role type from this list, since creating any
additional roles with type initiator would lead to broken integrity,
as the authentication details/registration attributes are used by the
plugin already to set the initiator, and only one initiator can (and
must) be set on case.
  • Loading branch information
sergei-maertens committed Nov 29, 2024
1 parent a481f81 commit 46385d3
Show file tree
Hide file tree
Showing 8 changed files with 454 additions and 6 deletions.
17 changes: 17 additions & 0 deletions src/openforms/contrib/zgw/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ class DocumentTypeSerializer(serializers.Serializer):
)


class RoleTypeSerializer(serializers.Serializer):
description = serializers.CharField(
label=_("description"),
help_text=_(
"The description/label given to the role type in the Catalogi API. It "
"identifies the role type within a case type."
),
)
description_generic = serializers.CharField(
label=_("generic description"),
help_text=_(
"One of the pre-determined generic descriptions, such as 'behandelaar' "
"or 'belanghebbende'."
),
)


class CaseTypeProductSerializer(serializers.Serializer):
url = serializers.CharField(
label=_("url"),
Expand Down
76 changes: 74 additions & 2 deletions src/openforms/contrib/zgw/clients/catalogi.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class CaseType(TypedDict):
concept: NotRequired[bool]
productenOfDiensten: list[str] # URL pointers to products
informatieobjecttypen: NotRequired[list[str]] # URL pointers to document types
roltypen: NotRequired[list[str]] # URL pointers to role types


class InformatieObjectType(TypedDict):
Expand All @@ -63,6 +64,28 @@ class InformatieObjectType(TypedDict):
concept: NotRequired[bool]


type PublicationStatusFilter = Literal["alles", "concept", "definitief"]

type RoleDescriptionGeneric = Literal[
"adviseur",
"behandelaar",
"belanghebbende",
"beslisser",
"initiator",
"klantcontacter",
"zaakcoordinator",
"mede_initiator",
]


class RoleType(TypedDict):
url: str
zaaktype: str
zaaktypeIdentificatie: str # since 1.2
omschrijving: str
omschrijvingGeneriek: RoleDescriptionGeneric


class EigenschapSpecificatie(TypedDict):
groep: NotRequired[str]
formaat: Literal["tekst", "getal", "datum", "datum_tijd"]
Expand Down Expand Up @@ -91,19 +114,28 @@ class Eigenschap(TypedDict):
class CaseTypeListParams(TypedDict, total=False):
catalogus: str
identificatie: str
status: Literal["alles", "concept", "definitief"]
status: PublicationStatusFilter
datumGeldigheid: str
page: int


class InformatieObjectTypeListParams(TypedDict, total=False):
catalogus: str
status: Literal["alles", "concept", "definitief"]
status: PublicationStatusFilter
omschrijving: str
datumGeldigheid: str
page: int


class RoleTypeListParams(TypedDict, total=False):
zaaktype: str
zaaktypeIdentificatie: str # from 1.2 onwards
omschrijvingGeneriek: RoleDescriptionGeneric
status: PublicationStatusFilter
datumGeldigheid: str
page: int


class CatalogiClient(NLXClient):
_api_version: CatalogiAPIVersion | None = None

Expand Down Expand Up @@ -325,6 +357,46 @@ def list_statustypen(self, zaaktype: str) -> list[dict]:
results = response.json()["results"]
return results

def get_all_role_types(
self,
*,
catalogus: str,
within_casetype: str,
) -> Iterator[RoleType]:
params: RoleTypeListParams = {
"zaaktypeIdentificatie": within_casetype,
}
if self.allow_drafts:
params["status"] = "alles"

# get the case types so that we are filtering within the right catalogue, as
# the same case type identification may be defined in different catalogues
case_type_versions = (
self.find_case_types(
catalogus=catalogus,
identification=within_casetype,
)
or []
)
all_valid_roltype_urls: list[str] = sum(
(
case_type_version.get("roltypen", [])
for case_type_version in case_type_versions
),
[],
)
if not all_valid_roltype_urls:
return []

response = self.get("roltypen", params=params) # type: ignore
response.raise_for_status()
data: PaginatedResponseData[RoleType] = response.json()

for role_type in pagination_helper(self, data):
if role_type["url"] not in all_valid_roltype_urls:
continue
yield role_type

def list_roltypen(
self,
zaaktype: str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def get_fields(self):
return fields


class ListProductsQueryParamsSerializer(ZGWAPIGroupMixin, serializers.Serializer):
class FilterForCaseTypeQueryParamsSerializer(ZGWAPIGroupMixin, serializers.Serializer):
catalogue_url = serializers.URLField(
label=_("catalogus URL"),
help_text=_("Filter case types against this catalogue URL."),
Expand Down
2 changes: 2 additions & 0 deletions src/openforms/registrations/contrib/zgw_apis/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
CatalogueListView,
DocumentTypesListView,
ProductsListView,
RoleTypeListView,
)

app_name = "zgw_apis"
Expand All @@ -13,5 +14,6 @@
path("catalogues", CatalogueListView.as_view(), name="catalogue-list"),
path("case-types", CaseTypesListView.as_view(), name="case-type-list"),
path("document-types", DocumentTypesListView.as_view(), name="document-type-list"),
path("role-types", RoleTypeListView.as_view(), name="role-type-list"),
path("products", ProductsListView.as_view(), name="product-list"),
]
59 changes: 56 additions & 3 deletions src/openforms/registrations/contrib/zgw_apis/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from openforms.contrib.zgw.api.serializers import (
CaseTypeProductSerializer,
CaseTypeSerializer,
RoleTypeSerializer,
)
from openforms.contrib.zgw.api.views import (
BaseCatalogueListView,
Expand All @@ -21,9 +22,9 @@

from .filters import (
APIGroupQueryParamsSerializer,
FilterForCaseTypeQueryParamsSerializer,
ListCaseTypesQueryParamsSerializer,
ListDocumentTypesQueryParamsSerializer,
ListProductsQueryParamsSerializer,
)


Expand Down Expand Up @@ -90,6 +91,58 @@ class DocumentTypesListView(BaseDocumentTypesListView):
filter_serializer_class = ListDocumentTypesQueryParamsSerializer


@dataclass
class RoleType:
description: str
description_generic: str


@extend_schema_view(
get=extend_schema(
summary=_(
"List the available role types bound to a case type within a catalogue "
"(ZGW APIs)"
),
parameters=[FilterForCaseTypeQueryParamsSerializer],
),
)
class RoleTypeListView(ListMixin[RoleType], APIView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAdminUser,)
serializer_class = RoleTypeSerializer

def get_objects(self) -> list[RoleType]:
filter_serializer = FilterForCaseTypeQueryParamsSerializer(
data=self.request.query_params
)
filter_serializer.is_valid(raise_exception=True)

catalogue_url = filter_serializer.validated_data["catalogue_url"]
case_type_identification = filter_serializer.validated_data[
"case_type_identification"
]
role_types: list[RoleType] = []
with filter_serializer.get_ztc_client() as client:
_role_types = client.get_all_role_types(
catalogus=catalogue_url,
within_casetype=case_type_identification,
)
for _role_type in _role_types:
# skip over initiator, since that one is set automatically and there can
# only be one
if (
description_generic := _role_type["omschrijvingGeneriek"]
) == "initiator":
continue
role_type = RoleType(
description=_role_type["omschrijving"],
description_generic=description_generic,
)
if role_type not in role_types:
role_types.append(role_type)
return role_types


@dataclass
class Product:
url: str
Expand All @@ -101,7 +154,7 @@ class Product:
summary=_(
"List the available products bound to a case type within a catalogue (ZGW APIs)"
),
parameters=[ListProductsQueryParamsSerializer],
parameters=[FilterForCaseTypeQueryParamsSerializer],
),
)
class ProductsListView(ListMixin[Product], APIView):
Expand All @@ -110,7 +163,7 @@ class ProductsListView(ListMixin[Product], APIView):
serializer_class = CaseTypeProductSerializer

def get_objects(self):
filter_serializer = ListProductsQueryParamsSerializer(
filter_serializer = FilterForCaseTypeQueryParamsSerializer(
data=self.request.query_params
)
filter_serializer.is_valid(raise_exception=True)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Authorization:
- Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0X2NsaWVudF9pZCIsImlhdCI6MTczMjg3ODI2NSwiY2xpZW50X2lkIjoidGVzdF9jbGllbnRfaWQiLCJ1c2VyX2lkIjoiIiwidXNlcl9yZXByZXNlbnRhdGlvbiI6IiJ9.aRL5d8Nql-yZ7H1suAQBHAY6YwEZx9-qxsJd_5B80XY
Connection:
- keep-alive
User-Agent:
- python-requests/2.32.2
method: GET
uri: http://localhost:8003/catalogi/api/v1/catalogussen?domein=VRSN
response:
body:
string: '{"count":0,"next":null,"previous":null,"results":[]}'
headers:
API-version:
- 1.3.1
Allow:
- GET, POST, HEAD, OPTIONS
Content-Length:
- '52'
Content-Type:
- application/json
Cross-Origin-Opener-Policy:
- same-origin
Referrer-Policy:
- same-origin
Vary:
- Accept, origin
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Authorization:
- Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0X2NsaWVudF9pZCIsImlhdCI6MTczMjg3ODI2NSwiY2xpZW50X2lkIjoidGVzdF9jbGllbnRfaWQiLCJ1c2VyX2lkIjoiIiwidXNlcl9yZXByZXNlbnRhdGlvbiI6IiJ9.aRL5d8Nql-yZ7H1suAQBHAY6YwEZx9-qxsJd_5B80XY
Connection:
- keep-alive
User-Agent:
- python-requests/2.32.2
method: GET
uri: http://localhost:8003/catalogi/api/v1/zaaktypen?catalogus=http%3A%2F%2Flocalhost%3A8003%2Fcatalogi%2Fapi%2Fv1%2Fcatalogussen%2Fbd58635c-793e-446d-a7e0-460d7b04829d&identificatie=i-do-not-exist
response:
body:
string: '{"count":0,"next":null,"previous":null,"results":[]}'
headers:
API-version:
- 1.3.1
Allow:
- GET, POST, HEAD, OPTIONS
Content-Length:
- '52'
Content-Type:
- application/json
Cross-Origin-Opener-Policy:
- same-origin
Referrer-Policy:
- same-origin
Vary:
- Accept, origin
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
status:
code: 200
message: OK
version: 1
Loading

0 comments on commit 46385d3

Please sign in to comment.