Skip to content

Commit e143d2a

Browse files
pi-sigmaswrichards
authored andcommittedDec 5, 2024
[#2903] Support import of Zaaktype configs without library
1 parent 167fc35 commit e143d2a

File tree

6 files changed

+161
-41
lines changed

6 files changed

+161
-41
lines changed
 

‎src/open_inwoner/openzaak/admin.py

+80-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from solo.admin import SingletonModelAdmin
1818

1919
from open_inwoner.ckeditor5.widgets import CKEditorWidget
20-
from open_inwoner.openzaak.import_export import CatalogusConfigImport, ZGWConfigExport
20+
from open_inwoner.openzaak.import_export import ZGWConfigExport, ZGWConfigImport
2121
from open_inwoner.utils.forms import LimitedUploadFileField
2222

2323
from .models import (
@@ -131,7 +131,7 @@ def get_urls(self):
131131
path(
132132
"import-catalogus-dump/",
133133
self.admin_site.admin_view(self.process_file_view),
134-
name="upload_zgw_import_file",
134+
name="upload_catalogus_import_file",
135135
),
136136
]
137137
return custom_urls + urls
@@ -161,7 +161,7 @@ def process_file_view(self, request):
161161

162162
try:
163163
import_result = (
164-
CatalogusConfigImport.import_from_jsonl_file_in_django_storage(
164+
ZGWConfigImport.import_from_jsonl_file_in_django_storage(
165165
target_file_name,
166166
storage,
167167
)
@@ -371,6 +371,7 @@ def has_delete_permission(self, request, obj=None):
371371

372372
@admin.register(ZaakTypeConfig)
373373
class ZaakTypeConfigAdmin(admin.ModelAdmin):
374+
change_list_template = "admin/zaaktypeconfig_change_list.html"
374375
inlines = [
375376
ZaakTypeInformatieObjectTypeConfigInline,
376377
ZaakTypeStatusTypeConfigInline,
@@ -434,6 +435,17 @@ class ZaakTypeConfigAdmin(admin.ModelAdmin):
434435
]
435436
ordering = ("identificatie", "catalogus__domein")
436437

438+
def get_urls(self):
439+
urls = super().get_urls()
440+
custom_urls = [
441+
path(
442+
"import-zaaktype-dump/",
443+
self.admin_site.admin_view(self.process_file_view),
444+
name="upload_zaaktype_import_file",
445+
),
446+
]
447+
return custom_urls + urls
448+
437449
@admin.action(description=_("Export to file"))
438450
def export_zaaktype_configs(modeladmin, request, queryset):
439451
export = ZGWConfigExport.from_zaaktype_configs(queryset)
@@ -446,6 +458,71 @@ def export_zaaktype_configs(modeladmin, request, queryset):
446458
] = 'attachment; filename="zgw-zaaktype-export.json"'
447459
return response
448460

461+
def process_file_view(self, request):
462+
form = ImportZGWExportFileForm()
463+
464+
if request.method == "POST":
465+
form = ImportZGWExportFileForm(request.POST, request.FILES)
466+
if form.is_valid():
467+
storage = PrivateMediaFileSystemStorage()
468+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
469+
target_file_name = f"zgw_import_dump_{timestamp}.json"
470+
storage.save(target_file_name, request.FILES["zgw_export_file"])
471+
472+
try:
473+
import_result = (
474+
ZGWConfigImport.import_from_jsonl_file_in_django_storage(
475+
target_file_name,
476+
storage,
477+
)
478+
)
479+
self.message_user(
480+
request,
481+
_(
482+
"%(num_rows)d item(s) processed in total, with %(error_rows)d failing row(s)."
483+
% {
484+
"num_rows": import_result.total_rows_processed,
485+
"error_rows": len(import_result.import_errors),
486+
}
487+
),
488+
messages.SUCCESS
489+
if not import_result.import_errors
490+
else messages.WARNING,
491+
)
492+
if errors := import_result.import_errors:
493+
msgs_deduped = set(error.__str__() for error in errors)
494+
error_msg_iterator = ([msg] for msg in msgs_deduped)
495+
496+
error_msg_html = format_html_join(
497+
"\n", "<p> - {}</p>", error_msg_iterator
498+
)
499+
error_msg_html = format_html(
500+
_("It was not possible to import the following items:")
501+
+ f"<div>{error_msg_html}</div>"
502+
)
503+
self.message_user(request, error_msg_html, messages.ERROR)
504+
505+
return HttpResponseRedirect(
506+
reverse(
507+
"admin:openzaak_zaaktypeconfig_changelist",
508+
)
509+
)
510+
except Exception:
511+
logger.exception("Unable to process ZGW import")
512+
self.message_user(
513+
request,
514+
_(
515+
"We were unable to process your upload. Please regenerate the file and try again."
516+
),
517+
messages.ERROR,
518+
)
519+
finally:
520+
storage.delete(target_file_name)
521+
522+
return TemplateResponse(
523+
request, "admin/import_zgw_export_form.html", {"form": form}
524+
)
525+
449526
def has_add_permission(self, request):
450527
return False
451528

‎src/open_inwoner/openzaak/import_export.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def from_zaaktype_configs(cls, zaaktype_configs: QuerySet) -> Self:
263263

264264

265265
@dataclasses.dataclass(frozen=True)
266-
class CatalogusConfigImport:
266+
class ZGWConfigImport:
267267
"""Import CatalogusConfig(s) and all associated relations."""
268268

269269
total_rows_processed: int = 0

‎src/open_inwoner/openzaak/tests/test_admin.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def test_import_flow_reports_success(self) -> None:
191191

192192
form = self.app.get(
193193
reverse(
194-
"admin:upload_zgw_import_file",
194+
"admin:upload_catalogus_import_file",
195195
),
196196
user=self.user,
197197
).form
@@ -218,13 +218,13 @@ def test_import_flow_reports_success(self) -> None:
218218
)
219219

220220
@mock.patch(
221-
"open_inwoner.openzaak.import_export.CatalogusConfigImport.import_from_jsonl_file_in_django_storage"
221+
"open_inwoner.openzaak.import_export.ZGWConfigImport.import_from_jsonl_file_in_django_storage"
222222
)
223223
def test_import_flow_errors_reports_failure_to_user(self, m) -> None:
224224
m.side_effect = Exception("something went wrong")
225225
form = self.app.get(
226226
reverse(
227-
"admin:upload_zgw_import_file",
227+
"admin:upload_catalogus_import_file",
228228
),
229229
user=self.user,
230230
).form
@@ -254,7 +254,7 @@ def test_import_flow_errors_reports_failure_to_user(self, m) -> None:
254254
self.assertEqual(
255255
response.request.path,
256256
reverse(
257-
"admin:upload_zgw_import_file",
257+
"admin:upload_catalogus_import_file",
258258
),
259259
)
260260

@@ -272,7 +272,7 @@ def test_import_flow_reports_errors(self) -> None:
272272

273273
form = self.app.get(
274274
reverse(
275-
"admin:upload_zgw_import_file",
275+
"admin:upload_catalogus_import_file",
276276
),
277277
user=self.user,
278278
).form
@@ -333,7 +333,7 @@ def test_import_flow_reports_partial_errors(self) -> None:
333333

334334
form = self.app.get(
335335
reverse(
336-
"admin:upload_zgw_import_file",
336+
"admin:upload_catalogus_import_file",
337337
),
338338
user=self.user,
339339
).form

‎src/open_inwoner/openzaak/tests/test_import_export.py

+61-30
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.core.files.storage.memory import InMemoryStorage
66
from django.test import TestCase
77

8-
from open_inwoner.openzaak.import_export import CatalogusConfigImport, ZGWConfigExport
8+
from open_inwoner.openzaak.import_export import ZGWConfigExport, ZGWConfigImport
99
from open_inwoner.openzaak.models import (
1010
CatalogusConfig,
1111
ZaakTypeConfig,
@@ -342,20 +342,24 @@ def setUp(self):
342342
'{"model": "openzaak.zaaktypestatustypeconfig", "fields": {"zaaktype_config": ["ztc-id-a-0", "DM-0", "123456789"], "statustype_url": "https://bar.maykinmedia.nl", "omschrijving": "status omschrijving", "statustekst": "statustekst nieuw", "zaaktype_uuids": "[]", "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": ""}}',
343343
'{"model": "openzaak.zaaktyperesultaattypeconfig", "fields": {"zaaktype_config": ["ztc-id-a-0", "DM-0", "123456789"], "resultaattype_url": "https://bar.maykinmedia.nl", "omschrijving": "resultaat", "zaaktype_uuids": "[]", "description": "description new"}}',
344344
]
345+
self.json_dupes = [
346+
'{"model": "openzaak.zaaktypestatustypeconfig", "fields": {"zaaktype_config": ["ztc-id-a-0", "DM-0", "123456789"], "statustype_url": "https://bar.maykinmedia.nl", "omschrijving": "status omschrijving", "statustekst": "statustekst nieuw", "zaaktype_uuids": "[]", "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": ""}}',
347+
]
345348
self.jsonl = "\n".join(self.json_lines)
349+
self.jsonl_with_dupes = "\n".join(self.json_lines + self.json_dupes)
346350

347351
def test_import_jsonl_update_success(self):
348352
mocks = ZGWExportImportMockData()
349353
self.storage.save("import.jsonl", io.StringIO(self.jsonl))
350354

351-
import_result = CatalogusConfigImport.import_from_jsonl_file_in_django_storage(
355+
import_result = ZGWConfigImport.import_from_jsonl_file_in_django_storage(
352356
"import.jsonl", self.storage
353357
)
354358

355359
# check import
356360
self.assertEqual(
357361
import_result,
358-
CatalogusConfigImport(
362+
ZGWConfigImport(
359363
total_rows_processed=5,
360364
catalogus_configs_imported=1,
361365
zaaktype_configs_imported=1,
@@ -435,16 +439,16 @@ def test_import_jsonl_missing_statustype_config(self):
435439
# we use `asdict` and replace the Exceptions with string representations
436440
# because for Exceptions raised from within dataclasses, equality ==/is identity
437441
import_result = dataclasses.asdict(
438-
CatalogusConfigImport.import_from_jsonl_file_in_django_storage(
442+
ZGWConfigImport.import_from_jsonl_file_in_django_storage(
439443
"import.jsonl", self.storage
440444
)
441445
)
442446
expected_error = ZGWImportError(
443-
"ZaakTypeStatusTypeConfig not found in target environment: omschrijving = bogus, "
444-
"ZaakTypeConfig identificatie = ztc-id-a-0, Catalogus domein = DM-0, Catalogus rsin = 123456789"
447+
"ZaakTypeStatusTypeConfig not found in target environment: omschrijving = 'bogus', "
448+
"ZaakTypeConfig identificatie = 'ztc-id-a-0'"
445449
)
446450
import_expected = dataclasses.asdict(
447-
CatalogusConfigImport(
451+
ZGWConfigImport(
448452
total_rows_processed=6,
449453
catalogus_configs_imported=1,
450454
zaaktype_configs_imported=1,
@@ -481,16 +485,16 @@ def test_import_jsonl_update_statustype_config_missing_zt_config(self):
481485
# we use `asdict` and replace the Exceptions with string representations
482486
# because for Exceptions raised from within dataclasses, equality ==/is identity
483487
import_result = dataclasses.asdict(
484-
CatalogusConfigImport.import_from_jsonl_file_in_django_storage(
488+
ZGWConfigImport.import_from_jsonl_file_in_django_storage(
485489
"import.jsonl", self.storage
486490
)
487491
)
488492
expected_error = ZGWImportError(
489-
"ZaakTypeStatusTypeConfig not found in target environment: omschrijving = status omschrijving, "
490-
"ZaakTypeConfig identificatie = bogus, Catalogus domein = DM-1, Catalogus rsin = 666666666"
493+
"ZaakTypeStatusTypeConfig not found in target environment: omschrijving = 'status omschrijving', "
494+
"ZaakTypeConfig identificatie = 'bogus'"
491495
)
492496
import_expected = dataclasses.asdict(
493-
CatalogusConfigImport(
497+
ZGWConfigImport(
494498
total_rows_processed=6,
495499
catalogus_configs_imported=1,
496500
zaaktype_configs_imported=1,
@@ -513,7 +517,7 @@ def test_import_jsonl_update_statustype_config_missing_zt_config(self):
513517
self.assertEqual(ZaakTypeStatusTypeConfig.objects.count(), 1)
514518
self.assertEqual(ZaakTypeResultaatTypeConfig.objects.count(), 1)
515519

516-
def test_import_jsonl_update_reports_duplicates(self):
520+
def test_import_jsonl_update_reports_duplicate_db_records(self):
517521
mocks = ZGWExportImportMockData()
518522

519523
ZaakTypeResultaatTypeConfigFactory(
@@ -527,16 +531,16 @@ def test_import_jsonl_update_reports_duplicates(self):
527531
# we use `asdict` and replace the Exceptions with string representations
528532
# because for Exceptions raised from within dataclasses, equality ==/is identity
529533
import_result = dataclasses.asdict(
530-
CatalogusConfigImport.import_from_jsonl_file_in_django_storage(
534+
ZGWConfigImport.import_from_jsonl_file_in_django_storage(
531535
"import.jsonl", self.storage
532536
)
533537
)
534538
expected_error = ZGWImportError(
535-
"Got multiple results for ZaakTypeResultaatTypeConfig: omschrijving = resultaat, "
536-
"ZaakTypeConfig identificatie = ztc-id-a-0, Catalogus domein = DM-0, Catalogus rsin = 123456789"
539+
"Got multiple results for ZaakTypeResultaatTypeConfig: omschrijving = 'resultaat', "
540+
"ZaakTypeConfig identificatie = 'ztc-id-a-0'"
537541
)
538542
import_expected = dataclasses.asdict(
539-
CatalogusConfigImport(
543+
ZGWConfigImport(
540544
total_rows_processed=5,
541545
catalogus_configs_imported=1,
542546
zaaktype_configs_imported=1,
@@ -552,6 +556,39 @@ def test_import_jsonl_update_reports_duplicates(self):
552556
# check import
553557
self.assertEqual(import_result, import_expected)
554558

559+
def test_import_jsonl_update_reports_duplicate_natural_keys_in_upload_file(self):
560+
mocks = ZGWExportImportMockData()
561+
562+
self.storage.save("import.jsonl", io.StringIO(self.jsonl_with_dupes))
563+
564+
# we use `asdict` and replace the Exceptions with string representations
565+
# because for Exceptions raised from within dataclasses, equality ==/is identity
566+
import_result = dataclasses.asdict(
567+
ZGWConfigImport.import_from_jsonl_file_in_django_storage(
568+
"import.jsonl", self.storage
569+
)
570+
)
571+
expected_error = ZGWImportError(
572+
"ZaakTypeStatusTypeConfig was processed multiple times because it contains duplicate "
573+
"natural keys: omschrijving = 'status omschrijving', ZaakTypeConfig identificatie = 'ztc-id-a-0'"
574+
)
575+
import_expected = dataclasses.asdict(
576+
ZGWConfigImport(
577+
total_rows_processed=6,
578+
catalogus_configs_imported=1,
579+
zaaktype_configs_imported=1,
580+
zaak_informatie_object_type_configs_imported=1,
581+
zaak_status_type_configs_imported=1,
582+
zaak_resultaat_type_configs_imported=1,
583+
import_errors=[expected_error],
584+
),
585+
)
586+
import_result["import_errors"][0] = str(import_result["import_errors"][0])
587+
import_expected["import_errors"][0] = str(import_expected["import_errors"][0])
588+
589+
# check import
590+
self.assertEqual(import_result, import_expected)
591+
555592
def test_import_jsonl_fails_with_catalogus_domein_rsin_mismatch(self):
556593
service = ServiceFactory(slug="service-0")
557594
CatalogusConfigFactory(
@@ -569,18 +606,16 @@ def test_import_jsonl_fails_with_catalogus_domein_rsin_mismatch(self):
569606
with self.assertLogs(
570607
logger="open_inwoner.openzaak.import_export", level="ERROR"
571608
) as cm:
572-
import_result = CatalogusConfigImport.from_jsonl_stream_or_string(
573-
import_line
574-
)
609+
import_result = ZGWConfigImport.from_jsonl_stream_or_string(import_line)
575610
self.assertEqual(
576611
cm.output,
577612
[
578613
# error from trying to load existing CatalogusConfig
579614
"ERROR:open_inwoner.openzaak.import_export:"
580-
"CatalogusConfig not found in target environment: Domein = BAR, Rsin = 987654321",
615+
"CatalogusConfig not found in target environment: Domein = 'BAR', Rsin = '987654321'",
581616
# error from deserializing nested ZGW objects
582617
"ERROR:open_inwoner.openzaak.import_export:"
583-
"ZaakTypeConfig not found in target environment: Identificatie = ztc-id-a-0, Catalogus domein = DM-0, Catalogus rsin = 123456789",
618+
"ZaakTypeConfig not found in target environment: Identificatie = 'ztc-id-a-0', Catalogus domein = 'DM-0', Catalogus rsin = '123456789'",
584619
],
585620
)
586621

@@ -598,7 +633,7 @@ def test_import_jsonl_fails_with_catalogus_domein_rsin_mismatch(self):
598633
def test_bad_import_types(self):
599634
for bad_type in (set(), list(), b""):
600635
with self.assertRaises(ValueError):
601-
CatalogusConfigImport.from_jsonl_stream_or_string(bad_type)
636+
ZGWConfigImport.from_jsonl_stream_or_string(bad_type)
602637

603638
def test_valid_input_types_are_accepted(self):
604639
ZGWExportImportMockData()
@@ -609,10 +644,10 @@ def test_valid_input_types_are_accepted(self):
609644
self.jsonl,
610645
):
611646
with self.subTest(f"Input type {type(input)}"):
612-
import_result = CatalogusConfigImport.from_jsonl_stream_or_string(input)
647+
import_result = ZGWConfigImport.from_jsonl_stream_or_string(input)
613648
self.assertEqual(
614649
import_result,
615-
CatalogusConfigImport(
650+
ZGWConfigImport(
616651
total_rows_processed=5,
617652
catalogus_configs_imported=1,
618653
zaaktype_configs_imported=1,
@@ -628,9 +663,7 @@ def test_import_is_atomic(self):
628663
bad_jsonl = self.jsonl + "\n" + bad_line
629664

630665
with self.assertRaises(KeyError):
631-
CatalogusConfigImport.from_jsonl_stream_or_string(
632-
stream_or_string=bad_jsonl
633-
)
666+
ZGWConfigImport.from_jsonl_stream_or_string(stream_or_string=bad_jsonl)
634667

635668
counts = (
636669
CatalogusConfig.objects.count(),
@@ -654,8 +687,6 @@ def setUp(self):
654687

655688
def test_exports_can_be_imported(self):
656689
export = ZGWConfigExport.from_catalogus_configs(CatalogusConfig.objects.all())
657-
import_result = CatalogusConfigImport.from_jsonl_stream_or_string(
658-
export.as_jsonl()
659-
)
690+
import_result = ZGWConfigImport.from_jsonl_stream_or_string(export.as_jsonl())
660691

661692
self.assertEqual(import_result.total_rows_processed, 5)

‎src/open_inwoner/templates/admin/catalogusconfig_change_list.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{% block object-tools-items %}
55
{{ block.super }}
66
<li>
7-
<a href="{% url 'admin:upload_zgw_import_file' %}" class="addlink">
7+
<a href="{% url 'admin:upload_catalogus_import_file' %}" class="addlink">
88
{% trans "Import from file" %}
99
</a>
1010
</li>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{% extends "admin/change_list.html" %}
2+
{% load i18n admin_urls %}
3+
4+
{% block object-tools-items %}
5+
{{ block.super }}
6+
<li>
7+
<a href="{% url 'admin:upload_zaaktype_import_file' %}" class="addlink">
8+
{% trans "Import from file" %}
9+
</a>
10+
</li>
11+
{% endblock %}
12+

0 commit comments

Comments
 (0)
Please sign in to comment.