diff --git a/src/open_inwoner/openzaak/admin.py b/src/open_inwoner/openzaak/admin.py index 95e5336465..bab3b3cad3 100644 --- a/src/open_inwoner/openzaak/admin.py +++ b/src/open_inwoner/openzaak/admin.py @@ -19,6 +19,7 @@ UserCaseStatusNotification, ZaakTypeConfig, ZaakTypeInformatieObjectTypeConfig, + ZaakTypeResultaatTypeConfig, ZaakTypeStatusTypeConfig, ) from .resources.import_resource import StatusTranslationImportResource @@ -202,11 +203,37 @@ def has_delete_permission(self, request, obj=None): return request.user.is_superuser +class ZaakTypeResultaattypeConfigInline(admin.TabularInline): + model = ZaakTypeResultaatTypeConfig + fields = [ + "omschrijving", + "resultaattype_url", + "zaaktype_uuids", + "description", + ] + readonly_fields = [ + "omschrijving", + "resultaattype_url", + "zaaktype_uuids", + ] + ordering = ( + "zaaktype_uuids", + "omschrijving", + ) + + def has_add_permission(self, request, obj): + return False + + def has_delete_permission(self, request, obj=None): + return request.user.is_superuser + + @admin.register(ZaakTypeConfig) class ZaakTypeConfigAdmin(admin.ModelAdmin): inlines = [ ZaakTypeInformatieObjectTypeConfigInline, ZaakTypeStatusTypeConfigInline, + ZaakTypeResultaattypeConfigInline, ] actions = [ "mark_as_notify_status_changes", diff --git a/src/open_inwoner/openzaak/catalog.py b/src/open_inwoner/openzaak/catalog.py index de40998b06..c0a13b23ed 100644 --- a/src/open_inwoner/openzaak/catalog.py +++ b/src/open_inwoner/openzaak/catalog.py @@ -83,6 +83,26 @@ def fetch_single_status_type(status_type_url: str) -> Optional[StatusType]: return status_type +@cache_result( + "resultaat_type:{resultaat_type_url}", timeout=settings.CACHE_ZGW_CATALOGI_TIMEOUT +) +def fetch_single_resultaat_type(resultaat_type_url: str) -> Optional[ResultaatType]: + client = build_client("catalogi") + + if client is None: + return + + try: + response = client.retrieve("resultaattype", url=resultaat_type_url) + except (RequestException, ClientError) as e: + logger.exception("exception while making request", exc_info=e) + return + + resultaat_type = factory(ResultaatType, response) + + return resultaat_type + + # not cached because only used by tools, # and because caching (stale) listings can break lookups def fetch_zaaktypes_no_cache() -> List[ZaakType]: diff --git a/src/open_inwoner/openzaak/management/commands/zgw_import_data.py b/src/open_inwoner/openzaak/management/commands/zgw_import_data.py index d032c319a7..e2915b4ebb 100644 --- a/src/open_inwoner/openzaak/management/commands/zgw_import_data.py +++ b/src/open_inwoner/openzaak/management/commands/zgw_import_data.py @@ -6,6 +6,7 @@ import_catalog_configs, import_zaaktype_configs, import_zaaktype_informatieobjecttype_configs, + import_zaaktype_resultaattype_configs, import_zaaktype_statustype_configs, ) @@ -15,7 +16,38 @@ class Command(BaseCommand): help = "Import ZGW catalog data" + def log_supplement_imports_to_stdout( + self, import_func: callable, config_type: str + ) -> None: + """ + Convenience function for logging zaaktype config types to stdout + + Example input: + import_func=import_zaaktype_informatieobjecttype_configs + config_type="informatiebjecttype" + + Example output: + imported 3 new zaaktype-informatiebjecttype configs + AAA - zaaktype-aaa + info-aaa-1 + info-aaa-2 + BBB - zaaktype-bbb + info-bbb + """ + imported = import_func() + + count = sum(len(t[1]) for t in imported) + self.stdout.write(f"imported {count} new zaaktype-{config_type} configs") + + for ztc, config_types in sorted(imported, key=lambda t: str(t[0])): + self.stdout.write(str(ztc)) + for c in sorted(map(str, config_types)): + self.stdout.write(f" {c}") + + self.stdout.write("") + def handle(self, *args, **options): + # catalogus config imported = import_catalog_configs() self.stdout.write(f"imported {len(imported)} new catalogus configs") @@ -24,6 +56,7 @@ def handle(self, *args, **options): self.stdout.write("") + # zaaktype config imported = import_zaaktype_configs() self.stdout.write(f"imported {len(imported)} new zaaktype configs") @@ -32,24 +65,13 @@ def handle(self, *args, **options): self.stdout.write("") - imported = import_zaaktype_informatieobjecttype_configs() - - count = sum(len(t[1]) for t in imported) - - self.stdout.write(f"imported {count} new zaaktype-informatiebjecttype configs") - for ztc, info_types in sorted(imported, key=lambda t: str(t[0])): - self.stdout.write(str(ztc)) - for c in sorted(map(str, info_types)): - self.stdout.write(f" {c}") - - self.stdout.write("") - - imported = import_zaaktype_statustype_configs() - - count = sum(len(t[1]) for t in imported) - - self.stdout.write(f"imported {count} new zaaktype-statustype configs") - for ztc, status_types in sorted(imported, key=lambda t: str(t[0])): - self.stdout.write(str(ztc)) - for c in sorted(map(str, status_types)): - self.stdout.write(f" {c}") + # supplemental configs + self.log_supplement_imports_to_stdout( + import_zaaktype_informatieobjecttype_configs, "informatiebjecttype" + ) + self.log_supplement_imports_to_stdout( + import_zaaktype_statustype_configs, "statustype" + ) + self.log_supplement_imports_to_stdout( + import_zaaktype_resultaattype_configs, "resultaattype" + ) diff --git a/src/open_inwoner/openzaak/migrations/0027_zaaktype_resultaattype_config.py b/src/open_inwoner/openzaak/migrations/0027_zaaktype_resultaattype_config.py new file mode 100644 index 0000000000..9b32306884 --- /dev/null +++ b/src/open_inwoner/openzaak/migrations/0027_zaaktype_resultaattype_config.py @@ -0,0 +1,71 @@ +# Generated by Django 3.2.20 on 2023-10-26 10:05 + +from django.db import migrations, models +import django.db.models.deletion +import django_better_admin_arrayfield.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("openzaak", "0026_zaaktypestatustypeconfig_document_upload_description"), + ] + + operations = [ + migrations.CreateModel( + name="ZaakTypeResultaatTypeConfig", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "resultaattype_url", + models.URLField(max_length=1000, verbose_name="Resultaattype URL"), + ), + ( + "omschrijving", + models.CharField(max_length=20, verbose_name="Omschrijving"), + ), + ( + "zaaktype_uuids", + django_better_admin_arrayfield.models.fields.ArrayField( + base_field=models.UUIDField(verbose_name="Zaaktype UUID"), + default=list, + size=None, + ), + ), + ( + "description", + models.TextField( + blank=True, + default="", + help_text="Determines the text that will be shown to the user if a case is set to this result", + verbose_name="Frontend description", + ), + ), + ( + "zaaktype_config", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="openzaak.zaaktypeconfig", + ), + ), + ], + options={ + "verbose_name": "Zaaktype Resultaattype Configuration", + }, + ), + migrations.AddConstraint( + model_name="zaaktyperesultaattypeconfig", + constraint=models.UniqueConstraint( + fields=("zaaktype_config", "resultaattype_url"), + name="unique_zaaktype_config_resultaattype_url", + ), + ), + ] diff --git a/src/open_inwoner/openzaak/models.py b/src/open_inwoner/openzaak/models.py index 727398dcb3..909a524dab 100644 --- a/src/open_inwoner/openzaak/models.py +++ b/src/open_inwoner/openzaak/models.py @@ -377,6 +377,50 @@ def __str__(self): return f"{self.zaaktype_config.identificatie} - {self.omschrijving}" +class ZaakTypeResultaatTypeConfig(models.Model): + zaaktype_config = models.ForeignKey( + "openzaak.ZaakTypeConfig", + on_delete=models.CASCADE, + ) + resultaattype_url = models.URLField( + verbose_name=_("Resultaattype URL"), + max_length=1000, + ) + omschrijving = models.CharField( + verbose_name=_("Omschrijving"), + max_length=20, + ) + zaaktype_uuids = ArrayField( + models.UUIDField( + verbose_name=_("Zaaktype UUID"), + ), + default=list, + ) + + # configuration + description = models.TextField( + blank=True, + default="", + verbose_name=_("Frontend description"), + help_text=_( + "Determines the text that will be shown to the user if a case is set to this result" + ), + ) + + class Meta: + verbose_name = _("Zaaktype Resultaattype Configuration") + + constraints = [ + UniqueConstraint( + name="unique_zaaktype_config_resultaattype_url", + fields=["zaaktype_config", "resultaattype_url"], + ) + ] + + def __str__(self): + return f"{self.zaaktype_config.identificatie} - {self.omschrijving}" + + class UserCaseStatusNotificationBase(models.Model): user = models.ForeignKey( "accounts.User", diff --git a/src/open_inwoner/openzaak/tests/test_zgw_imports_command.py b/src/open_inwoner/openzaak/tests/test_zgw_imports_command.py index 88542d6a81..21b71a6e31 100644 --- a/src/open_inwoner/openzaak/tests/test_zgw_imports_command.py +++ b/src/open_inwoner/openzaak/tests/test_zgw_imports_command.py @@ -12,6 +12,7 @@ OpenZaakConfig, ZaakTypeConfig, ZaakTypeInformatieObjectTypeConfig, + ZaakTypeResultaatTypeConfig, ZaakTypeStatusTypeConfig, ) from open_inwoner.openzaak.tests.factories import ServiceFactory @@ -49,6 +50,7 @@ def test_zgw_import_data_command(self, m): self.assertEqual(ZaakTypeConfig.objects.count(), 2) self.assertEqual(ZaakTypeInformatieObjectTypeConfig.objects.count(), 3) self.assertEqual(ZaakTypeStatusTypeConfig.objects.count(), 2) + self.assertEqual(ZaakTypeResultaatTypeConfig.objects.count(), 2) stdout = out.getvalue().strip() @@ -73,6 +75,12 @@ def test_zgw_import_data_command(self, m): AAA - zaaktype-aaa AAA - status-aaa-1 AAA - status-aaa-2 + + imported 2 new zaaktype-resultaattype configs + AAA - zaaktype-aaa + AAA - test + BBB - zaaktype-bbb + BBB - test """ ).strip() @@ -87,6 +95,7 @@ def test_zgw_import_data_command(self, m): self.assertEqual(ZaakTypeConfig.objects.count(), 2) self.assertEqual(ZaakTypeInformatieObjectTypeConfig.objects.count(), 3) self.assertEqual(ZaakTypeStatusTypeConfig.objects.count(), 2) + self.assertEqual(ZaakTypeResultaatTypeConfig.objects.count(), 2) stdout = out.getvalue().strip() @@ -99,6 +108,8 @@ def test_zgw_import_data_command(self, m): imported 0 new zaaktype-informatiebjecttype configs imported 0 new zaaktype-statustype configs + + imported 0 new zaaktype-resultaattype configs """ ).strip() @@ -111,7 +122,7 @@ def test_zgw_import_data_command_without_catalog(self, m): ) InformationObjectTypeMockData().install_mocks(m, with_catalog=False) - # run it to import our data + # # run it to import our data out = StringIO() call_command("zgw_import_data", stdout=out) @@ -119,6 +130,7 @@ def test_zgw_import_data_command_without_catalog(self, m): self.assertEqual(ZaakTypeConfig.objects.count(), 2) self.assertEqual(ZaakTypeInformatieObjectTypeConfig.objects.count(), 3) self.assertEqual(ZaakTypeStatusTypeConfig.objects.count(), 2) + self.assertEqual(ZaakTypeResultaatTypeConfig.objects.count(), 2) stdout = out.getvalue().strip() @@ -141,6 +153,12 @@ def test_zgw_import_data_command_without_catalog(self, m): AAA - zaaktype-aaa AAA - status-aaa-1 AAA - status-aaa-2 + + imported 2 new zaaktype-resultaattype configs + AAA - zaaktype-aaa + AAA - test + BBB - zaaktype-bbb + BBB - test """ ).strip() @@ -155,6 +173,7 @@ def test_zgw_import_data_command_without_catalog(self, m): self.assertEqual(ZaakTypeConfig.objects.count(), 2) self.assertEqual(ZaakTypeInformatieObjectTypeConfig.objects.count(), 3) self.assertEqual(ZaakTypeStatusTypeConfig.objects.count(), 2) + self.assertEqual(ZaakTypeResultaatTypeConfig.objects.count(), 2) stdout = out.getvalue().strip() @@ -167,6 +186,8 @@ def test_zgw_import_data_command_without_catalog(self, m): imported 0 new zaaktype-informatiebjecttype configs imported 0 new zaaktype-statustype configs + + imported 0 new zaaktype-resultaattype configs """ ).strip() diff --git a/src/open_inwoner/openzaak/tests/test_zgw_imports_iotypes.py b/src/open_inwoner/openzaak/tests/test_zgw_imports_iotypes.py index 1612fa80c4..79f3984017 100644 --- a/src/open_inwoner/openzaak/tests/test_zgw_imports_iotypes.py +++ b/src/open_inwoner/openzaak/tests/test_zgw_imports_iotypes.py @@ -88,6 +88,18 @@ def __init__(self): statustypen=[ self.statustype_aaa_1["url"], ], + resultaattypen=[ + f"{CATALOGI_ROOT}resultaatypen/b1a268dd-4322-47bb-a930-b83066b4a32c" + ], + ) + self.resultaat_type_1 = generate_oas_component( + "ztc", + "schemas/ResultaatType", + url=f"{CATALOGI_ROOT}resultaatypen/b1a268dd-4322-47bb-a930-b83066b4a32c", + zaaktype=self.zaaktype_aaa_1, + omschrijving="test", + resultaattypeomschrijving="test1", + selectielijstklasse="ABC", ) self.zaaktype_bbb = generate_oas_component( "ztc", @@ -103,6 +115,9 @@ def __init__(self): self.info_type_bbb["url"], ], statustypen=[], + resultaattypen=[ + f"{CATALOGI_ROOT}resultaatypen/b1a268dd-4322-47bb-a930-b83066b4a32c" + ], ) self.zaaktype_aaa_2 = generate_oas_component( "ztc", @@ -121,6 +136,9 @@ def __init__(self): statustypen=[ self.statustype_aaa_2["url"], ], + resultaattypen=[ + f"{CATALOGI_ROOT}resultaatypen/b1a268dd-4322-47bb-a930-b83066b4a32c", + ], ) self.zaaktype_aaa_intern = generate_oas_component( "ztc", @@ -137,6 +155,7 @@ def __init__(self): self.info_type_aaa_1["url"], ], statustypen=[], + resultaattypen=[], ) self.extra_zaaktype_aaa = generate_oas_component( "ztc", @@ -155,6 +174,9 @@ def __init__(self): self.extra_info_type_aaa_3["url"], ], statustypen=[], + resultaattypen=[ + self.resultaat_type_1["url"], + ], ) self.all_io_types = [ @@ -174,6 +196,9 @@ def __init__(self): self.statustype_aaa_1, self.statustype_aaa_2, ] + self.all_resultaat_types = [ + self.resultaat_type_1, + ] def setUpOASMocks(self, m): mock_service_oas_get(m, CATALOGI_ROOT, "ztc") @@ -188,6 +213,7 @@ def install_mocks(self, m, *, with_catalog=True) -> "InformationObjectTypeMockDa self.extra_info_type_aaa_3, self.statustype_aaa_1, self.statustype_aaa_2, + self.resultaat_type_1, ]: m.get(resource["url"], json=resource) @@ -202,6 +228,14 @@ def install_mocks(self, m, *, with_catalog=True) -> "InformationObjectTypeMockDa ] ), ) + m.get( + f"{CATALOGI_ROOT}resultaattypen", + json=paginated_response( + [ + self.resultaat_type_1, + ] + ), + ) if with_catalog: cat_a = f"&catalogus={CATALOGI_ROOT}catalogussen/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" diff --git a/src/open_inwoner/openzaak/zgw_imports.py b/src/open_inwoner/openzaak/zgw_imports.py index 38d6b3a276..038a5fcac1 100644 --- a/src/open_inwoner/openzaak/zgw_imports.py +++ b/src/open_inwoner/openzaak/zgw_imports.py @@ -3,13 +3,18 @@ from django.db import transaction -from zgw_consumers.api_models.catalogi import InformatieObjectType, StatusType +from zgw_consumers.api_models.catalogi import ( + InformatieObjectType, + ResultaatType, + StatusType, +) from open_inwoner.openzaak.api_models import ZaakType from open_inwoner.openzaak.catalog import ( fetch_case_types_by_identification_no_cache, fetch_catalogs_no_cache, fetch_single_information_object_type, + fetch_single_resultaat_type, fetch_single_status_type, fetch_zaaktypes_no_cache, ) @@ -17,6 +22,7 @@ CatalogusConfig, ZaakTypeConfig, ZaakTypeInformatieObjectTypeConfig, + ZaakTypeResultaatTypeConfig, ZaakTypeStatusTypeConfig, ) @@ -148,6 +154,20 @@ def import_zaaktype_statustype_configs() -> List[Tuple[ZaakTypeConfig, StatusTyp return created +def import_zaaktype_resultaattype_configs() -> List[ + Tuple[ZaakTypeConfig, ResultaatType] +]: + """ + generate ZaakTypeResultaatTypeConfigs for all ZaakTypeConfig + """ + created = [] + for ztc in ZaakTypeConfig.objects.all(): + imported = import_resultaattype_configs_for_type(ztc) + if imported: + created.append((ztc, imported)) + return created + + def import_zaaktype_informatieobjecttype_configs_for_type( ztc: ZaakTypeConfig, ) -> List[ZaakTypeInformatieObjectTypeConfig]: @@ -243,7 +263,7 @@ def import_statustype_configs_for_type( for zaaktype_statustype in ztc.zaaktypestatustypeconfig_set.all() } - # collect and implicitly de-duplicate informatieobjecttype url's and track which zaaktype used it + # collect and implicitly de-duplicate statustype url's and track which zaaktype used it info_queue = defaultdict(list) for zaak_type in zaak_types: for url in zaak_type.statustypen: @@ -282,3 +302,70 @@ def import_statustype_configs_for_type( ZaakTypeStatusTypeConfig.objects.bulk_update(update, ["zaaktype_uuids"]) return create + + +def import_resultaattype_configs_for_type( + ztc: ZaakTypeConfig, +) -> List[ZaakTypeResultaatTypeConfig]: + """ + generate ZaakTypeResultaatTypeConfigs for all ResultaatTypes used by each ZaakTypeConfigs source ZaakTypes + + this is a bit complicated because one ZaakTypeConfig can represent multiple ZaakTypes + """ + + # grab actual ZaakTypes for this identificatie + zaak_types: List[ZaakType] = get_configurable_zaaktypes_by_identification( + ztc.identificatie, ztc.catalogus_url + ) + if not zaak_types: + return [] + + create = [] + update = [] + + with transaction.atomic(): + # map existing config records by url + + info_map = { + zaaktype_resultaattype.resultaattype_url: zaaktype_resultaattype + for zaaktype_resultaattype in ztc.zaaktyperesultaattypeconfig_set.all() + } + + # collect and implicitly de-duplicate resultaattype url's and track which zaaktype used it + info_queue = defaultdict(list) + for zaak_type in zaak_types: + for url in zaak_type.resultaattypen: + info_queue[url].append(zaak_type) + + if info_queue: + # load urls and update/create records + for resultaattype_url, using_zaak_types in info_queue.items(): + resultaat_type = fetch_single_resultaat_type(resultaattype_url) + + zaaktype_resultaattype = info_map.get(resultaat_type.url) + if zaaktype_resultaattype: + # we got a record for this, see if we got data to update + for using in using_zaak_types: + # track which zaaktype UUID's are interested in this resultaattype + if using.uuid not in zaaktype_resultaattype.zaaktype_uuids: + zaaktype_resultaattype.zaaktype_uuids.append(using.uuid) + if zaaktype_resultaattype not in create: + update.append(zaaktype_resultaattype) + else: + # new record + zaaktype_resultaattype = ZaakTypeResultaatTypeConfig( + zaaktype_config=ztc, + resultaattype_url=resultaat_type.url, + omschrijving=resultaat_type.omschrijving, + zaaktype_uuids=[zt.uuid for zt in using_zaak_types], + ) + create.append(zaaktype_resultaattype) + # not strictly necessary but let's be accurate + info_map[resultaat_type.uuid] = zaaktype_resultaattype + + if create: + ZaakTypeResultaatTypeConfig.objects.bulk_create(create) + if update: + ZaakTypeResultaatTypeConfig.objects.bulk_update(update, ["zaaktype_uuids"]) + + return create