From 10a15d38f38c6895523e4013518755e1066c97c5 Mon Sep 17 00:00:00 2001 From: MatthijsBekendam <47739550+MatthijsBekendam@users.noreply.github.com> Date: Tue, 22 Aug 2023 12:22:55 +0200 Subject: [PATCH] Issue/#2192 inclusions (#267) **Enhancements, Bug Fixes, and Code Refactoring** - **Added:** Extension of list call functionality to accept expand parameters for enriched data retrieval. - **Added:** Introduction of 'ExpandField' class to manage and process expand parameters. - **Added:** 'Inclusion' class to facilitate data inclusion and enhance response customization. - **Added:** Method for building inclusion schemas. - **Added:** Recursion logic in later stages to support nested data inclusion. - **Added:** 'Inclusions' class for streamlined management of included external API calls. - **Added:** External API calls within the 'Inclusions' class for comprehensive data integration. - **Added:** Clear and descriptive text to the expand filter for better understanding. - **Added:** Validation mechanism and resolved issues with external API calls within 'Inclusions'. - **Added:** Regex validator to enhance input validation. - **Added:** Explanation regarding the use of regex validation. - **Added:** Support for nested dictionary types like 'relevante_andere_zaken' in the expansions model. - **Added:** Integration of the 'expand' feature into the OpenAPI Specification (OAS) and expanded GET detail endpoints. - **Changed:** Hard-coded mappings from 'expensions.py' for increased flexibility. - **Changed:** Relocated 'routers' import into a function to address circular import concerns. - **Changed:** Reworked the expansions model to resolve previous errors and improve reliability. - **Changed:** Various issues related to specific problem reports: #2280, #2279, #2269, #2270, #2271, #2288, #2287. - **Changed:** Updated OAS description and resolved a bug in the expansions. - **Updated:** Requirements files, including 'psyopg2' for proper functionality. These changes encompass a wide range of enhancements, bug fixes, and code clean-up, significantly improving the functionality, stability, and maintainability of the system. The introduction of expand parameters, 'ExpandField' class, 'Expand' class, and various bug fixes contribute to a more robust and efficient system. --- INSTALL.rst | 2 +- src/__init__.py | 0 src/zrc/api/expansions.py | 648 ++++++++++++++++++++++++++++++++ src/zrc/api/filters.py | 18 + src/zrc/api/tests/test_zaken.py | 166 ++++++++ src/zrc/api/utils.py | 5 +- src/zrc/api/viewsets.py | 3 + src/zrc/conf/includes/api.py | 4 +- src/zrc/sync/signals.py | 1 - 9 files changed, 840 insertions(+), 7 deletions(-) create mode 100644 src/__init__.py create mode 100644 src/zrc/api/expansions.py diff --git a/INSTALL.rst b/INSTALL.rst index 94734833..fff7018c 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,4 +1,4 @@ -============ +pip============ Installation ============ diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/zrc/api/expansions.py b/src/zrc/api/expansions.py new file mode 100644 index 00000000..2cbf1928 --- /dev/null +++ b/src/zrc/api/expansions.py @@ -0,0 +1,648 @@ +import json +import logging +import re +import uuid +from dataclasses import dataclass +from urllib.request import Request, urlopen + +from django.contrib.contenttypes.models import ContentType +from django.urls import resolve +from django.urls.exceptions import Resolver404 +from django.utils.translation import ugettext_lazy as _ + +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema +from rest_framework import serializers + +logger = logging.getLogger(__name__) + + +def is_uri(s): + try: + from urllib.parse import urlparse + + result = urlparse(s) + return all([result.scheme, result.netloc]) + except: + return False + + +@dataclass +class ExpansionField: + def __init__( + self, + id, + parent, + sub_field_parent, + sub_field, + level, + struc_type, + value, + is_empty, + code=None, + parent_code=None, + ): + self.id: str = id + self.parent: str = parent + self.sub_field_parent: str = sub_field_parent + self.sub_field: str = sub_field + self.level: int = level + self.type: str = struc_type + self.value: dict = value + self.is_empty: bool = is_empty + self.code = code + self.parent_code = parent_code + + +EXPAND_QUERY_PARAM = OpenApiParameter( + name="expand", + location=OpenApiParameter.QUERY, + description="Haal details van gelinkte resources direct op. Als je meerdere resources tegelijk wilt ophalen kun je deze scheiden met een komma. Voor het ophalen van resources die een laag dieper genest zijn wordt de punt-notatie gebruikt.", + type=OpenApiTypes.STR, + examples=[ + OpenApiExample(name="expand zaaktype", value="zaaktype"), + OpenApiExample(name="expand status", value="status"), + OpenApiExample(name="expand status.statustype", value="status.statustype"), + OpenApiExample( + name="expand hoofdzaak.status.statustype", + value="hoofdzaak.status.statustype", + ), + OpenApiExample( + name="expand hoofdzaak.deelzaken.status.statustype", + value="hoofdzaak.deelzaken.status.statustype", + ), + ], +) + + +class ExpansionMixin: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.expanded_fields = [] + self.called_external_uris = {} + self.expanded_fields_all = [] + + @extend_schema(parameters=[EXPAND_QUERY_PARAM]) + def retrieve(self, request, *args, **kwargs): + response = super().retrieve(request, *args, **kwargs) + response = self.inclusions(response) + return response + + def list(self, request, *args, **kwargs): + response = super().list(request, *args, **kwargs) + response = self.inclusions(response) + return response + + @staticmethod + def _convert_to_internal_url(url: str) -> str: + """Convert external uri (https://testserver/api/v1...) to internal uri (/api/v1...)""" + keyword = "api" + save_sentence = False + internal_url = "/" + if isinstance(url, dict): + if not url.get("url", None): + for key, value in url.items(): + if is_uri(value): + url = value + break + else: + url = url.get("url", None) + if not url: + return "" + for word in url.split("/"): + if word == keyword: + save_sentence = True + if save_sentence: + internal_url += word + "/" + + return internal_url[:-1] + + def _get_external_data(self, url): + if isinstance(url, dict): + if not url.get("url", None): + for key, value in url.items(): + if is_uri(value): + url = value + break + else: + url = url.get("url", None) + if not self.called_external_uris.get(url, None): + try: + access_token = self.request.jwt_auth.encoded + # access_token = "eyJhbGciOiJIUzI1NiIsImNsaWVudF9pZGVudGlmaWVyIjoiYWxsdGhlc2NvcGVzYXJlYmVsb25ndG91czIyMjIyMzEzMjUzMi14eXBhcGRTV3FqMVQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhbGx0aGVzY29wZXNhcmViZWxvbmd0b3VzMjIyMjIzMTMyNTMyLXh5cGFwZFNXcWoxVCIsImlhdCI6MTY5MjYwMTAwNiwiY2xpZW50X2lkIjoiYWxsdGhlc2NvcGVzYXJlYmVsb25ndG91czIyMjIyMzEzMjUzMi14eXBhcGRTV3FqMVQiLCJ1c2VyX2lkIjoiIiwidXNlcl9yZXByZXNlbnRhdGlvbiI6IiJ9.iOCqIi9IxTfTjBmk8YuQLK7Wc1LlzVUsiJPFIijEc0s" + headers = {"Authorization": f"Bearer {access_token}"} + + with urlopen(Request(url, headers=headers)) as response: + data = json.loads(response.read().decode("utf8")) + self.called_external_uris[url] = data + return data + + except: + self.called_external_uris[url] = {} + return {} + else: + return self.called_external_uris[url] + + def _get_internal_data(self, url): + resolver_match = resolve(self._convert_to_internal_url(url)) + + uuid = resolver_match.kwargs["uuid"] + + kwargs = {"uuid": uuid} + + content_type = ContentType.objects.get( + model=resolver_match.func.initkwargs["basename"] + ) + + obj = content_type.get_object_for_this_type(**kwargs) + + serializer = resolver_match.func.cls.serializer_class + + serializer_exp_field = serializer(obj, context={"request": self.request}) + + return serializer_exp_field.data + + def get_data( + self, + url: str, + ) -> dict: + """Get data from external url or from local database""" + + try: + return self._get_internal_data(url) + except Resolver404: + return self._get_external_data(url) + except Exception as e: + logger.error( + f"The following error occured while trying to get data from {url}: {e}" + ) + return {} + + def build_expand_schema( + self, + result: dict, + fields_to_expand: list, + ): + """Build the expand schema for the response. First, the fields to expand are split on the "." character. Then, the first part of the split is used to get the urls from the result. The urls are then used to get the corresponding data from the external api or from the local database. The data is then gathered/collected inside a list consisted of namedtuples. When all data is collected, it calls the _build_json method which builds the json response.""" + expansion = {"_expand": {}} + + for exp_field in fields_to_expand: + loop_id = str(uuid.uuid4()) + self.expanded_fields = [] + for depth, sub_field in enumerate(exp_field.split(".")): + if depth == 0: + try: + urls = result[self.convert_camel_to_snake(sub_field)] + except KeyError: + try: + urls = result[sub_field] + except KeyError: + raise self.validation_invalid_expand_field(sub_field) + + if isinstance(urls, list): + for x in urls: + self._add_to_expanded_fields( + loop_id, + exp_field, + sub_field, + depth, + data=self.get_data(x), + struc_type="list", + is_empty=False, + original_data=result, + ) + if not urls: + expansion["_expand"][sub_field] = [] + else: + if urls: + self._add_to_expanded_fields( + loop_id, + exp_field, + sub_field, + depth, + data=self.get_data(urls), + struc_type="dict", + is_empty=False, + original_data=result, + ) + else: + expansion["_expand"][sub_field] = {} + + else: + + for field in self.expanded_fields: + if ( + field.sub_field == exp_field.split(".")[depth - 1] + and field.level == depth - 1 + ): + if self.next_iter_if_value_is_empty(field.value): + continue + try: + urls = field.value[ + self.convert_camel_to_snake(sub_field) + ] + except KeyError: + try: + urls = field.value[sub_field] + except KeyError: + raise self.validation_invalid_expand_field( + sub_field + ) + if isinstance(urls, list): + if urls: + for x in urls: + self._add_to_expanded_fields( + loop_id, + exp_field, + sub_field, + depth, + data=self.get_data(x), + struc_type="list", + is_empty=False, + original_data=result, + field=field, + ) + else: + self._add_to_expanded_fields( + loop_id, + exp_field, + sub_field, + depth, + data={}, + struc_type="list", + is_empty=True, + original_data=result, + field=field, + ) + else: + if urls: + self._add_to_expanded_fields( + loop_id, + exp_field, + sub_field, + depth, + data=self.get_data(urls), + struc_type="dict", + is_empty=False, + original_data=result, + field=field, + ) + + else: + self._add_to_expanded_fields( + loop_id, + exp_field, + sub_field, + depth, + data={}, + struc_type="dict", + is_empty=True, + original_data=result, + field=field, + ) + else: + if self.next_iter_if_value_is_empty(field.value): + field.is_empty = True + + if not self.expanded_fields: + continue + expansion = self._build_json(expansion) + + for key in ["loop_id", "depth", "code", "parent_code"]: + self.remove_key(expansion, key) + + result["_expand"].update(expansion["_expand"]) + + def _build_json(self, expansion: dict) -> dict: + max_value = max(self.expanded_fields, key=lambda x: x.level).level + for i in range(max_value + 1): + specific_levels = [x for x in self.expanded_fields if x.level == i] + + for index, fields_of_level in enumerate(specific_levels): + if index == 0 and i == 0: + if fields_of_level.type == "list" and not expansion["_expand"].get( + fields_of_level.sub_field, None + ): + expansion["_expand"][fields_of_level.sub_field] = [] + elif fields_of_level.type == "dict" and not expansion[ + "_expand" + ].get(fields_of_level.sub_field, None): + expansion["_expand"][fields_of_level.sub_field] = {} + + if i == 0: + if fields_of_level.type == "list": + skip = False + for field_dict in expansion["_expand"][ + fields_of_level.sub_field + ]: + if fields_of_level.value["url"] == field_dict["url"]: + skip = True + if not skip: + expansion["_expand"][fields_of_level.sub_field].append( + fields_of_level.value + ) + + elif fields_of_level.type == "dict" and not expansion[ + "_expand" + ].get(fields_of_level.sub_field, None): + + expansion["_expand"][ + fields_of_level.sub_field + ] = fields_of_level.value + + else: + match = self.get_parent_dict( + expansion["_expand"], + target_key1="url", + target_key3="code", + target_value1=fields_of_level.parent, + target_value3=fields_of_level.parent_code, + level=i, + field_level=fields_of_level.level, + ) + + if not match: + continue + + for parent_dict in match: + if isinstance(parent_dict, str): + if parent_dict != fields_of_level.sub_field_parent: + continue + parent_dict = match[parent_dict] + if parent_dict.get("url", None) != fields_of_level.parent: + continue + + if not parent_dict.get("_expand", {}) and isinstance( + parent_dict[fields_of_level.sub_field], list + ): + + parent_dict["_expand"] = {fields_of_level.sub_field: []} + elif not parent_dict.get("_expand", {}).get( + fields_of_level.sub_field, None + ) and isinstance(parent_dict[fields_of_level.sub_field], list): + + parent_dict["_expand"].update( + {fields_of_level.sub_field: []} + ) + + elif not parent_dict.get("_expand", {}) and isinstance( + parent_dict[fields_of_level.sub_field], str + ): + parent_dict["_expand"] = {fields_of_level.sub_field: {}} + + elif not parent_dict.get("_expand", {}).get( + fields_of_level.sub_field, None + ) and isinstance(parent_dict[fields_of_level.sub_field], str): + parent_dict["_expand"].update( + {fields_of_level.sub_field: {}} + ) + + if isinstance(parent_dict[fields_of_level.sub_field], list): + add = True + for expand in parent_dict["_expand"][ + fields_of_level.sub_field + ]: + if expand["url"] == fields_of_level.value["url"]: + add = False + + if add: + if fields_of_level.is_empty: + parent_dict["_expand"][ + fields_of_level.sub_field + ] = [] + else: + parent_dict["_expand"][ + fields_of_level.sub_field + ].append(fields_of_level.value) + + elif isinstance(parent_dict[fields_of_level.sub_field], str): + if fields_of_level.is_empty: + parent_dict["_expand"].update( + {fields_of_level.sub_field: {}} + ) + else: + parent_dict["_expand"].update( + {fields_of_level.sub_field: fields_of_level.value} + ) + elif parent_dict[fields_of_level.sub_field] is None: + try: + parent_dict["_expand"].update( + {fields_of_level.sub_field: fields_of_level.value} + ) + except KeyError: + parent_dict["_expand"] = {fields_of_level.sub_field: {}} + + return expansion + + def get_parent_dict( + self, + data, + target_key1, + target_key3, + target_value1, + target_value3, + level, + field_level, + parent=None, + ): + """Get the parent dictionary of the target value.""" + + if isinstance(data, dict): + if target_value3: + to_compare = bool( + data.get(target_key1) == target_value1 + and data.get(target_key3) == target_value3 + and data.get("depth") == level - 1 + ) + elif not target_value3: + to_compare = bool( + data.get(target_key1) == target_value1 + and data.get("depth") == level - 1 + ) + + if to_compare: + return parent + + for key, value in data.items(): + if isinstance(value, (dict, list)): + parent_dict = self.get_parent_dict( + value, + target_key1, + target_key3, + target_value1, + target_value3, + level, + field_level, + parent=data, + ) + if parent_dict is not None: + return parent_dict + + elif isinstance(data, list): + for item in data: + if isinstance(item, (dict, list)): + parent_dict = self.get_parent_dict( + item, + target_key1, + target_key3, + target_value1, + target_value3, + level, + field_level, + parent=data, + ) + if parent_dict is not None: + return parent_dict + return None + + def remove_key(self, data, target_key): + if isinstance(data, dict): + for key in list(data.keys()): + if key == target_key: + del data[key] + elif isinstance(data[key], (dict, list)): + self.remove_key(data[key], target_key) + elif isinstance(data, list): + for item in data: + if isinstance(item, (dict, list)): + self.remove_key(item, target_key) + + def inclusions(self, response): + expand_filter = self.request.query_params.get("expand", "") + if expand_filter: + fields_to_expand = expand_filter.split(",") + if self.action == "list": + for response_data in response.data["results"]: + response_data["_expand"] = {} + self.build_expand_schema( + response_data, + fields_to_expand, + ) + elif self.action == "retrieve": + response.data["_expand"] = {} + self.build_expand_schema(response.data, fields_to_expand) + + return response + + @staticmethod + def convert_camel_to_snake(string): + # Insert underscore before each capital letter + import re + + snake_case = re.sub(r"(? 0 else unique_code, + ) + + field_to_add.value["loop_id"] = loop_id + field_to_add.value["depth"] = field_to_add.level + field_to_add.value["parent_code"] = field.code if depth > 0 else unique_code + field_to_add.value["code"] = unique_code if depth > 0 else unique_code + self.expanded_fields.append(field_to_add) + self.expanded_fields_all.append(field_to_add) + + +class ExpandFieldValidator: + MAX_STEPS_DEPTH = 10 + MAX_EXPANDED_FIELDS = 20 + REGEX = r"^[\w']+([.,][\w']+)*$" # regex checks for field names separated by . or , (e.g "rollen,rollen.statussen") + + def _validate_maximum_depth_reached(self, expanded_fields): + """Validate maximum iterations to prevent infinite recursion""" + for expand_combination in expanded_fields.split(","): + if len(expand_combination.split(".")) > self.MAX_STEPS_DEPTH: + raise serializers.ValidationError( + { + "expand": _( + f"The submitted fields have surpassed its maximum recursion limit of {self.MAX_STEPS_DEPTH}" + ) + }, + code="recursion-limit", + ) + elif len(expanded_fields.split(",")) > self.MAX_EXPANDED_FIELDS: + raise serializers.ValidationError( + { + "expand": _( + f"The submitted expansion string has surpassed its maximum limit of {self.MAX_EXPANDED_FIELDS}" + ) + }, + code="max-str-length", + ) + + def _validate_regex(self, expanded_fields): + if not re.match(self.REGEX, expanded_fields): + raise serializers.ValidationError( + { + "expand": _( + f"The submitted expand fields do not match the required regex of {self.REGEX}" + ) + }, + code="expand-format-error", + ) + + def list(self, request, *args, **kwargs): + expand_filter = request.query_params.get("expand", "") + + if not request.query_params or not expand_filter: + return super().list(request, *args, **kwargs) + + self._validate_regex(expand_filter) + self._validate_maximum_depth_reached(expand_filter) + return super().list(request, *args, **kwargs) diff --git a/src/zrc/api/filters.py b/src/zrc/api/filters.py index 300fb389..055987d6 100644 --- a/src/zrc/api/filters.py +++ b/src/zrc/api/filters.py @@ -1,6 +1,8 @@ from django.utils.translation import ugettext_lazy as _ from django_filters import filters +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from vng_api_common.constants import VertrouwelijkheidsAanduiding from vng_api_common.filtersets import FilterSet from vng_api_common.utils import get_field_attribute, get_help_text @@ -22,6 +24,11 @@ def get_most_recent_status(queryset, name, value): return queryset.order_by("-datum_status_gezet")[:1] +def expand_filter(queryset, name, value): + """expansion filter logic is placed at view level""" + return queryset + + class MaximaleVertrouwelijkheidaanduidingFilter(filters.ChoiceFilter): def __init__(self, *args, **kwargs): kwargs.setdefault("choices", VertrouwelijkheidsAanduiding.choices) @@ -126,6 +133,17 @@ class ZaakFilter(FilterSet): help_text="Het veld waarop de resultaten geordend worden.", ) + expand = extend_schema_field(OpenApiTypes.STR)( + filters.CharFilter( + method=expand_filter, + help_text=_( + "Examples: \n" + "`expand=zaaktype, status, status.statustype, hoofdzaak.status.statustype, hoofdzaak.deelzaken.status.statustype`\n" + "Haal details van gelinkte resources direct op. Als je meerdere resources tegelijk wilt ophalen kun je deze scheiden met een komma. Voor het ophalen van resources die een laag dieper genest zijn wordt de punt-notatie gebruikt.", + ), + ) + ) + class Meta: model = Zaak fields = { diff --git a/src/zrc/api/tests/test_zaken.py b/src/zrc/api/tests/test_zaken.py index 24db5b00..a9928cef 100644 --- a/src/zrc/api/tests/test_zaken.py +++ b/src/zrc/api/tests/test_zaken.py @@ -1,4 +1,5 @@ import unittest +import uuid from datetime import date, timedelta from unittest.mock import patch @@ -15,6 +16,7 @@ RolOmschrijving, RolTypes, VertrouwelijkheidsAanduiding, + ZaakobjectTypes, ) from vng_api_common.tests import ( JWTAuthMixin, @@ -31,6 +33,7 @@ NatuurlijkPersoon, NietNatuurlijkPersoon, OrganisatorischeEenheid, + RelevanteZaakRelatie, Vestiging, Zaak, ) @@ -59,6 +62,7 @@ SCOPE_ZAKEN_CREATE, SCOPEN_ZAKEN_HEROPENEN, ) +from .test_zaakobject import OBJECT # ZTC ZTC_ROOT = "https://example.com/ztc/api/v1" @@ -1149,6 +1153,168 @@ def test_rol_vestiging_vestigings_nummer(self): self.assertEqual(response.data["count"], 1) +@override_settings( + LINK_FETCHER="vng_api_common.mocks.link_fetcher_200", + ZDS_CLIENT_CLASS="vng_api_common.mocks.MockClient", +) +class ZakenExpandTests(ZaakInformatieObjectSyncMixin, JWTAuthMixin, APITestCase): + heeft_alle_autorisaties = True + ZTC_ROOT = "https://example.com/ztc/api/v1" + CATALOGUS = f"{ZTC_ROOT}/catalogus/878a3318-5950-4642-8715-189745f91b04" + ZAAKTYPE = f"{CATALOGUS}/zaaktypen/283ffaf5-8470-457b-8064-90e5728f413f" + EIGENSCHAP = f"{ZTC_ROOT}/eigenschappen/f420c3e0-8345-44d9-a981-0f424538b9e9" + ZAAKOBJECTTYPE = ( + "http://testserver/api/v1/zaakobjecttypen/c340323d-31a5-46b4-93e8-fdc2d621be13" + ) + INFORMATIEOBJECT = f"http://example.com/drc/api/v1/enkelvoudiginformatieobjecten/{uuid.uuid4().hex}" + + @override_settings(ZDS_CLIENT_CLASS="vng_api_common.mocks.MockClient") + @patch("vng_api_common.validators.fetcher") + @patch("vng_api_common.validators.obj_has_shape", return_value=True) + def test_list_expand_filter_few_levels_deep(self, *mocks): + zaak = ZaakFactory.create() + + # zaak.zaaktype = "https://catalogi-api.test.vng.cloud/api/v1/zaaktypen/7e3353ef-d5da-4c1d-9155-a79c89194121" + # + # zaak2 = ZaakFactory.create() + # zaak2.zaaktype = "https://catalogi-api.test.vng.cloud/api/v1/zaaktypen/ed15c69d-15cd-4bc7-bc1a-b5d21d45dc36" + # zaak2.save() + + url = reverse("zaak-detail", kwargs={"uuid": zaak.uuid}) + + zaakrelatie = RelevanteZaakRelatie.objects.create( + zaak=zaak, url=url, aard_relatie="test" + ) + zaak.relevante_andere_zaken.add(zaakrelatie) + zaak.save() + + zaakeigenschap = ZaakEigenschapFactory.create( + zaak=zaak, eigenschap=self.EIGENSCHAP, waarde="This is a value" + ) + zaakeigenschap2 = ZaakEigenschapFactory.create( + zaak=zaak, eigenschap=self.EIGENSCHAP, waarde="This is a value" + ) + zaakeigenschap3 = ZaakEigenschapFactory.create( + zaak=zaak, eigenschap=self.EIGENSCHAP, waarde="This is a value" + ) + + # zaakeigenschap = ZaakEigenschapFactory.create( + # zaak=zaak2, eigenschap=self.EIGENSCHAP, waarde="This is a value" + # ) + zaakobject = ZaakObjectFactory.create( + zaak=zaak, + object=OBJECT, + object_type=ZaakobjectTypes.besluit, + zaakobjecttype=self.ZAAKOBJECTTYPE, + ) + # zaakobject = ZaakObjectFactory.create( + # zaak=zaak2, + # object=OBJECT, + # object_type=ZaakobjectTypes.besluit, + # zaakobjecttype=self.ZAAKOBJECTTYPE, + # ) + zio = ZaakInformatieObjectFactory.create(zaak=zaak) + # + rol = RolFactory.create( + zaak=zaak, + ) + # rol.roltype = "https://catalogi-api.test.vng.cloud/api/v1/roltypen/d03562e0-2feb-4d46-bf56-ed1d71122996" + # rol.save() + + # zaakobject.zaakobjecttype = "https://catalogi-api.test.vng.cloud/api/v1/zaakobjecttypen/f5a24710-6902-44f3-b4dd-9e75bd4c1403" + # zaakobject.save() + + status1 = StatusFactory.create(zaak=zaak) + rol.statussen.add(status1) + + rol2 = RolFactory.create( + zaak=zaak, + ) + rol3 = RolFactory.create( + zaak=zaak, + ) + rol4 = RolFactory.create( + zaak=zaak, + ) + rol2.statussen.add(status1) + + url = reverse("zaak-list") + expand_params = [ + "rollen.statussen.zaak.rollen,zaakinformatieobjecten,zaakobjecten.zaak,eigenschappen", + "relevanteAndereZaken.zaaktype", + "zaaktype.besluittypen,status.statustype,zaaktype.catalogus", + "rollen.zaak.rollen.zaak.rollen", + "zaaktype,rollen.statussen.zaak,status.zaak,zaakobjecten,zaakinformatieobjecten", + "zaaktype.catalogus.zaaktypen", + "zaaktype.besluittypen.zaaktypen", + "status.statustype,status.gezetdoor", + "zaaktype.gerelateerdeZaaktypen", + "zaaktype.zaakobjecttypen,zaaktype.statustypen", + "zaaktype.deelzaaktypen", + "zaaktype.eigenschappen.statustype", + "rollen.statussen,rollen.zaak", + "zaaktype.eigenschappen.catalogus,zaaktype.eigenschappen.zaaktype,zaaktype.eigenschappen.statustype", + "status,zaaktype", + "zaaktype,hoofdzaak,deelzaken,relevanteAndereZaken,eigenschappen,rollen,status,zaakobjecten,resultaat", + "status.zaak,status.statustype,status.gezetdoor", + "rollen.roltype", + "zaakobjecten.zaakobjecttype", + ] + for param in expand_params: + with self.subTest(param=param): + response = self.client.get( + url, + {"expand": param}, + **ZAAK_READ_KWARGS, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + # from pprint import pprint + # + # pprint(response.json()["results"][0]["_expand"]) + + @override_settings(ZDS_CLIENT_CLASS="vng_api_common.mocks.MockClient") + @patch("vng_api_common.validators.fetcher") + @patch("vng_api_common.validators.obj_has_shape", return_value=True) + def test_get_expand_filter_few_levels_deep(self, *mocks): + zaak = ZaakFactory.create() + zaak2 = ZaakFactory.create() + + url = reverse("zaak-detail", kwargs={"uuid": zaak.uuid}) + + zaakrelatie = RelevanteZaakRelatie.objects.create( + zaak=zaak, url=url, aard_relatie="test" + ) + zaak.relevante_andere_zaken.add(zaakrelatie) + zaak.save() + + zaakobject = ZaakObjectFactory.create( + zaak=zaak, + object=OBJECT, + object_type=ZaakobjectTypes.besluit, + zaakobjecttype=self.ZAAKOBJECTTYPE, + ) + + zio = ZaakInformatieObjectFactory.create(zaak=zaak) + + rol = RolFactory.create( + zaak=zaak, + ) + + status1 = StatusFactory.create(zaak=zaak) + rol.statussen.add(status1) + + rol2 = RolFactory.create( + zaak=zaak, + ) + + response = self.client.get( + url, + {"expand": "rollen.statussen,rollen.zaak"}, + **ZAAK_READ_KWARGS, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + class ZakenWerkVoorraadTests(JWTAuthMixin, APITestCase): """ Test that the queries to build up a 'werkvoorraad' work as expected. diff --git a/src/zrc/api/utils.py b/src/zrc/api/utils.py index 0b94c5f6..7ac30bc7 100644 --- a/src/zrc/api/utils.py +++ b/src/zrc/api/utils.py @@ -9,6 +9,5 @@ def get_absolute_url(url_name: str, uuid: str) -> str: url_name, kwargs={"version": settings.REST_FRAMEWORK["DEFAULT_VERSION"], "uuid": uuid}, ) - domain = Site.objects.get_current().domain - protocol = "https" if settings.IS_HTTPS else "http" - return f"{protocol}://{domain}{path}" + domain = settings.ZRC_BASE_URL + return f"{domain}{path}" diff --git a/src/zrc/api/viewsets.py b/src/zrc/api/viewsets.py index a4e1650c..801feaa8 100644 --- a/src/zrc/api/viewsets.py +++ b/src/zrc/api/viewsets.py @@ -48,6 +48,7 @@ from .audits import AUDIT_ZRC from .data_filtering import ListFilterByAuthorizationsMixin +from .expansions import ExpandFieldValidator, ExpansionMixin from .filters import ( KlantContactFilter, ResultaatFilter, @@ -195,6 +196,8 @@ class ZaakViewSet( GeoMixin, SearchMixin, CheckQueryParamsMixin, + ExpandFieldValidator, + ExpansionMixin, ListFilterByAuthorizationsMixin, viewsets.ModelViewSet, ): diff --git a/src/zrc/conf/includes/api.py b/src/zrc/conf/includes/api.py index a7987986..396822f6 100644 --- a/src/zrc/conf/includes/api.py +++ b/src/zrc/conf/includes/api.py @@ -8,7 +8,7 @@ REST_FRAMEWORK["PAGE_SIZE"] = 100 DOCUMENTATION_INFO_MODULE = "zrc.api.schema" - +ZRC_BASE_URL = os.getenv("ZRC_BASE_URL", "https://zaken-api.test.vng.cloud") SPECTACULAR_SETTINGS = BASE_SPECTACULAR_SETTINGS.copy() SPECTACULAR_SETTINGS.update( { @@ -17,7 +17,7 @@ # e.g. [{'url': 'https://example.com/v1', 'description': 'Text'}, ...] "SERVERS": [ { - "url": "https://zaken-api.vng.cloud/api/v1", + "url": "https://zaken-api.vng.cloud/api/v1/", "description": "Productie Omgeving", } ], diff --git a/src/zrc/sync/signals.py b/src/zrc/sync/signals.py index 27d61a71..f5d6ea8c 100644 --- a/src/zrc/sync/signals.py +++ b/src/zrc/sync/signals.py @@ -20,7 +20,6 @@ class SyncError(Exception): def sync_create_zio(relation: ZaakInformatieObject): zaak_url = get_absolute_url("zaak-detail", relation.zaak.uuid) - logger.info("Zaak: %s", zaak_url) logger.info("Informatieobject: %s", relation.informatieobject)