diff --git a/src/open_inwoner/openzaak/admin.py b/src/open_inwoner/openzaak/admin.py index c26478311..4dcd59bde 100644 --- a/src/open_inwoner/openzaak/admin.py +++ b/src/open_inwoner/openzaak/admin.py @@ -13,15 +13,11 @@ from django.utils.html import format_html, format_html_join from django.utils.translation import gettext_lazy as _, ngettext -from import_export.admin import ImportExportMixin from privates.storages import PrivateMediaFileSystemStorage from solo.admin import SingletonModelAdmin from open_inwoner.ckeditor5.widgets import CKEditorWidget -from open_inwoner.openzaak.import_export import ( - CatalogusConfigExport, - CatalogusConfigImport, -) +from open_inwoner.openzaak.import_export import CatalogusConfigImport, ZGWConfigExport from open_inwoner.utils.forms import LimitedUploadFileField from .models import ( @@ -144,7 +140,7 @@ def get_urls(self): @admin.action(description=_("Export to file")) def export_catalogus_configs(modeladmin, request, queryset): - export = CatalogusConfigExport.from_catalogus_configs(queryset) + export = ZGWConfigExport.from_catalogus_configs(queryset) response = StreamingHttpResponse( export.as_jsonl_iter(), content_type="application/json", @@ -374,7 +370,7 @@ def has_delete_permission(self, request, obj=None): @admin.register(ZaakTypeConfig) -class ZaakTypeConfigAdmin(ImportExportMixin, admin.ModelAdmin): +class ZaakTypeConfigAdmin(admin.ModelAdmin): inlines = [ ZaakTypeInformatieObjectTypeConfigInline, ZaakTypeStatusTypeConfigInline, @@ -383,6 +379,7 @@ class ZaakTypeConfigAdmin(ImportExportMixin, admin.ModelAdmin): actions = [ "mark_as_notify_status_changes", "mark_as_not_notify_status_changes", + "export_zaaktype_configs", ] fields = [ "urls", @@ -437,6 +434,18 @@ class ZaakTypeConfigAdmin(ImportExportMixin, admin.ModelAdmin): ] ordering = ("identificatie", "catalogus__domein") + @admin.action(description=_("Export to file")) + def export_zaaktype_configs(modeladmin, request, queryset): + export = ZGWConfigExport.from_zaaktype_configs(queryset) + response = StreamingHttpResponse( + export.as_jsonl_iter(), + content_type="application/json", + ) + response[ + "Content-Disposition" + ] = 'attachment; filename="zgw-zaaktype-export.json"' + return response + def has_add_permission(self, request): return False diff --git a/src/open_inwoner/openzaak/import_export.py b/src/open_inwoner/openzaak/import_export.py index 519bf36f2..3c04af641 100644 --- a/src/open_inwoner/openzaak/import_export.py +++ b/src/open_inwoner/openzaak/import_export.py @@ -152,11 +152,9 @@ def _update_nested_zgw_config( @dataclasses.dataclass(frozen=True) -class CatalogusConfigExport: - """Gather and export CatalogusConfig(s) and all associated relations.""" - +class ZGWConfigExport: catalogus_configs: QuerySet - zaak_type_configs: QuerySet + zaaktype_configs: QuerySet zaak_informatie_object_type_configs: QuerySet zaak_status_type_configs: QuerySet zaak_resultaat_type_configs: QuerySet @@ -164,7 +162,7 @@ class CatalogusConfigExport: def __iter__(self) -> Generator[QuerySet, Any, None]: yield from ( self.catalogus_configs, - self.zaak_type_configs, + self.zaaktype_configs, self.zaak_informatie_object_type_configs, self.zaak_status_type_configs, self.zaak_resultaat_type_configs, @@ -174,9 +172,32 @@ def __eq__(self, other: QuerySet) -> bool: for a, b in zip(self, other): if a.difference(b).exists(): return False - return True + def as_dicts_iter(self) -> Generator[dict, Any, None]: + for qs in self: + serialized_data = serializers.serialize( + queryset=qs, + format="json", + use_natural_foreign_keys=True, + use_natural_primary_keys=True, + ) + json_data: list[dict] = json.loads( + serialized_data, + ) + yield from json_data + + def as_jsonl_iter(self) -> Generator[str, Any, None]: + for row in self.as_dicts(): + yield json.dumps(row) + yield "\n" + + def as_dicts(self) -> list[dict]: + return list(self.as_dicts_iter()) + + def as_jsonl(self) -> str: + return "".join(self.as_jsonl_iter()) + @classmethod def from_catalogus_configs(cls, catalogus_configs: QuerySet) -> Self: if not isinstance(catalogus_configs, QuerySet): @@ -189,50 +210,56 @@ def from_catalogus_configs(cls, catalogus_configs: QuerySet) -> Self: f"`catalogus_configs` is of type {catalogus_configs.model}, not CatalogusConfig" ) - zaak_type_configs = ZaakTypeConfig.objects.filter( + zaaktype_configs = ZaakTypeConfig.objects.filter( catalogus__in=catalogus_configs ) informatie_object_types = ZaakTypeInformatieObjectTypeConfig.objects.filter( - zaaktype_config__in=zaak_type_configs + zaaktype_config__in=zaaktype_configs ) zaak_status_type_configs = ZaakTypeStatusTypeConfig.objects.filter( - zaaktype_config__in=zaak_type_configs + zaaktype_config__in=zaaktype_configs ) zaak_resultaat_type_configs = ZaakTypeResultaatTypeConfig.objects.filter( - zaaktype_config__in=zaak_type_configs + zaaktype_config__in=zaaktype_configs ) return cls( catalogus_configs=catalogus_configs, - zaak_type_configs=zaak_type_configs, + zaaktype_configs=zaaktype_configs, zaak_informatie_object_type_configs=informatie_object_types, zaak_status_type_configs=zaak_status_type_configs, zaak_resultaat_type_configs=zaak_resultaat_type_configs, ) - def as_dicts_iter(self) -> Generator[dict, Any, None]: - for qs in self: - serialized_data = serializers.serialize( - queryset=qs, - format="json", - use_natural_foreign_keys=True, - use_natural_primary_keys=True, - ) - json_data: list[dict] = json.loads( - serialized_data, + @classmethod + def from_zaaktype_configs(cls, zaaktype_configs: QuerySet) -> Self: + if not isinstance(zaaktype_configs, QuerySet): + raise TypeError( + f"`zaaktype_configs` is not a QuerySet, but a {type(zaaktype_configs)}" ) - yield from json_data - def as_jsonl_iter(self) -> Generator[str, Any, None]: - for row in self.as_dicts(): - yield json.dumps(row) - yield "\n" + if zaaktype_configs.model != ZaakTypeConfig: + raise ValueError( + f"`zaaktype_configs` is of type {zaaktype_configs.model}, not ZaakTypeConfig" + ) - def as_dicts(self) -> list[dict]: - return list(self.as_dicts_iter()) + informatie_object_types = ZaakTypeInformatieObjectTypeConfig.objects.filter( + zaaktype_config__in=zaaktype_configs + ) + zaak_status_type_configs = ZaakTypeStatusTypeConfig.objects.filter( + zaaktype_config__in=zaaktype_configs + ) + zaak_resultaat_type_configs = ZaakTypeResultaatTypeConfig.objects.filter( + zaaktype_config__in=zaaktype_configs + ) - def as_jsonl(self) -> str: - return "".join(self.as_jsonl_iter()) + return cls( + catalogus_configs=CatalogusConfig.objects.none(), + zaaktype_configs=zaaktype_configs, + zaak_informatie_object_type_configs=informatie_object_types, + zaak_status_type_configs=zaak_status_type_configs, + zaak_resultaat_type_configs=zaak_resultaat_type_configs, + ) @dataclasses.dataclass(frozen=True) diff --git a/src/open_inwoner/openzaak/tests/test_import_export.py b/src/open_inwoner/openzaak/tests/test_import_export.py index 50478d3e6..c8eb49572 100644 --- a/src/open_inwoner/openzaak/tests/test_import_export.py +++ b/src/open_inwoner/openzaak/tests/test_import_export.py @@ -5,10 +5,7 @@ from django.core.files.storage.memory import InMemoryStorage from django.test import TestCase -from open_inwoner.openzaak.import_export import ( - CatalogusConfigExport, - CatalogusConfigImport, -) +from open_inwoner.openzaak.import_export import CatalogusConfigImport, ZGWConfigExport from open_inwoner.openzaak.models import ( CatalogusConfig, ZaakTypeConfig, @@ -94,17 +91,17 @@ def test_export_only_accepts_queryset(self): for arg in (list(), set(), tuple(), None, "", CatalogusConfig.objects.first()): with self.subTest(f"Default factory should not accept {arg}"): with self.assertRaises(TypeError): - CatalogusConfigExport.from_catalogus_configs(arg) + ZGWConfigExport.from_catalogus_configs(arg) def test_export_only_accepts_queryset_of_correct_type(self): with self.assertRaises(ValueError): - CatalogusConfigExport.from_catalogus_configs(ZaakTypeConfig.objects.all()) + ZGWConfigExport.from_catalogus_configs(ZaakTypeConfig.objects.all()) def test_equality_operator(self): - export_a = CatalogusConfigExport.from_catalogus_configs( + export_a = ZGWConfigExport.from_catalogus_configs( CatalogusConfig.objects.filter(pk=self.mocks[0].catalogus.pk) ) - export_b = CatalogusConfigExport.from_catalogus_configs( + export_b = ZGWConfigExport.from_catalogus_configs( CatalogusConfig.objects.filter(pk=self.mocks[1].catalogus.pk) ) @@ -113,14 +110,14 @@ def test_equality_operator(self): self.assertFalse(export_a == export_b) def test_only_models_related_to_exported_catalogus_config_are_included(self): - export = CatalogusConfigExport.from_catalogus_configs( + export = ZGWConfigExport.from_catalogus_configs( CatalogusConfig.objects.filter(pk=self.mocks[0].catalogus.pk) ) for export_field, mock_field in zip( ( "catalogus_configs", - "zaak_type_configs", + "zaaktype_configs", "zaak_status_type_configs", "zaak_resultaat_type_configs", "zaak_informatie_object_type_configs", @@ -148,6 +145,79 @@ def test_only_models_related_to_exported_catalogus_config_are_included(self): ) +class ZaakTypeConfigExportTest(TestCase): + def setUp(self): + self.mocks = ZGWExportImportMockData(0) + + def test_export_zaaktype_configs(self): + export = ZGWConfigExport.from_zaaktype_configs( + ZaakTypeConfig.objects.filter(pk=self.mocks.ztc.pk) + ) + rows = export.as_dicts() + + expected = [ + { + "model": "openzaak.zaaktypeconfig", + "fields": { + "urls": '["https://foo.0.maykinmedia.nl"]', + "catalogus": ["DM-0", "123456789"], + "identificatie": "ztc-id-a-0", + "omschrijving": "zaaktypeconfig", + "notify_status_changes": False, + "description": "", + "external_document_upload_url": "", + "document_upload_enabled": False, + "contact_form_enabled": False, + "contact_subject_code": "", + "relevante_zaakperiode": None, + }, + }, + { + "model": "openzaak.zaaktypeinformatieobjecttypeconfig", + "fields": { + "zaaktype_config": ["ztc-id-a-0", "DM-0", "123456789"], + "informatieobjecttype_url": "https://foo.0.maykinmedia.nl", + "omschrijving": "informatieobject", + "zaaktype_uuids": '["a1591906-3368-470a-a957-4b8634c275a1"]', + "document_upload_enabled": False, + "document_notification_enabled": False, + }, + }, + { + "model": "openzaak.zaaktypestatustypeconfig", + "fields": { + "zaaktype_config": ["ztc-id-a-0", "DM-0", "123456789"], + "statustype_url": "https://foo.0.maykinmedia.nl", + "omschrijving": "status omschrijving", + "statustekst": "", + "zaaktype_uuids": '["a1591906-3368-470a-a957-4b8634c275a1"]', + "status_indicator": "", + "status_indicator_text": "", + "document_upload_description": "", + "description": "status", + "notify_status_change": True, + "action_required": False, + "document_upload_enabled": True, + "call_to_action_url": "", + "call_to_action_text": "", + "case_link_text": "", + }, + }, + { + "model": "openzaak.zaaktyperesultaattypeconfig", + "fields": { + "zaaktype_config": ["ztc-id-a-0", "DM-0", "123456789"], + "resultaattype_url": "https://foo.0.maykinmedia.nl", + "omschrijving": "resultaat", + "zaaktype_uuids": '["a1591906-3368-470a-a957-4b8634c275a1"]', + "description": "", + }, + }, + ] + + self.assertEqual(rows, expected) + + class TestCatalogusExport(TestCase): def setUp(self): self.mocks = [ @@ -156,7 +226,7 @@ def setUp(self): ] def test_export_catalogus_configs(self): - export = CatalogusConfigExport.from_catalogus_configs( + export = ZGWConfigExport.from_catalogus_configs( CatalogusConfig.objects.filter(pk=self.mocks[0].catalogus.pk) ) rows = export.as_dicts() @@ -233,9 +303,7 @@ def test_export_catalogus_configs(self): self.assertEqual(rows, expected) def test_export_catalogus_configs_as_jsonl(self): - export = CatalogusConfigExport.from_catalogus_configs( - CatalogusConfig.objects.all() - ) + export = ZGWConfigExport.from_catalogus_configs(CatalogusConfig.objects.all()) rows = list(export.as_jsonl_iter()) expected = [ @@ -585,9 +653,7 @@ def setUp(self): ZGWExportImportMockData() def test_exports_can_be_imported(self): - export = CatalogusConfigExport.from_catalogus_configs( - CatalogusConfig.objects.all() - ) + export = ZGWConfigExport.from_catalogus_configs(CatalogusConfig.objects.all()) import_result = CatalogusConfigImport.from_jsonl_stream_or_string( export.as_jsonl() )