From 74918b53394155b97fa2c3cd61429f9cca302312 Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Thu, 13 Aug 2026 17:27:32 -0700 Subject: [PATCH 1/2] initial testing for to1 pr07: import slider relationships from layer fixture --- layers/tests/test_fixture_import.py | 221 ++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/layers/tests/test_fixture_import.py b/layers/tests/test_fixture_import.py index 81c68d1..cda5c32 100644 --- a/layers/tests/test_fixture_import.py +++ b/layers/tests/test_fixture_import.py @@ -15,6 +15,8 @@ LayerXYZ, LookupInfo, MultilayerAssociation, + MultilayerDimension, + MultilayerDimensionValue, ) try: @@ -531,6 +533,225 @@ def test_missing_attribute_relation_uuid_raises_error_under_strict_policy(self): with self.assertRaises(ValueError): import_fixture_rows(fixture_rows, **self._import_kwargs()) + +class LayerFixtureImportPR07Test(TestCase): + """PR07 contract tests for multilayer import graph integrity.""" + + def _require_importer(self): + self.assertIsNotNone( + import_fixture_rows, + "importer API missing: expected layers.fixture_import.import_fixture_rows", + ) + + def _import_kwargs(self): + return { + "dry_run": False, + "associate_all_sites": True, + "missing_ref_policy": "error", + "duplicate_uuid_policy": "error", + } + + def _layer_fields(self, name, layer_type="WMS"): + return { + "name": name, + "layer_type": layer_type, + "slug_name": None, + "url": None, + } + + def test_multilayer_dimension_value_association_graph_resolves_by_uuid(self): + self._require_importer() + + parent_uuid = uuid4() + target_a_uuid = uuid4() + target_b_uuid = uuid4() + dimension_uuid = uuid4() + association_a_uuid = uuid4() + association_b_uuid = uuid4() + value_1_uuid = uuid4() + value_2_uuid = uuid4() + + fixture_rows = [ + build_node( + model="layers.layer", + source_pk=1001, + uuid_value=parent_uuid, + fields=self._layer_fields("Parent Slider Layer"), + relations={}, + ), + build_node( + model="layers.layer", + source_pk=1002, + uuid_value=target_a_uuid, + fields=self._layer_fields("Target A"), + relations={}, + ), + build_node( + model="layers.layer", + source_pk=1003, + uuid_value=target_b_uuid, + fields=self._layer_fields("Target B"), + relations={}, + ), + build_node( + model="layers.multilayerdimension", + source_pk=1101, + uuid_value=dimension_uuid, + fields={ + "name": "Year", + "label": "Year", + "order": 10, + "animated": True, + "angle_labels": False, + }, + relations={ + "layer": build_ref( + model="layers.layer", + source_pk=50101, + uuid_value=parent_uuid, + ) + }, + ), + build_node( + model="layers.multilayerassociation", + source_pk=1201, + uuid_value=association_a_uuid, + fields={"name": "A"}, + relations={ + "parentLayer": build_ref( + model="layers.layer", + source_pk=50201, + uuid_value=parent_uuid, + ), + "layer": build_ref( + model="layers.layer", + source_pk=50202, + uuid_value=target_a_uuid, + ), + }, + ), + build_node( + model="layers.multilayerassociation", + source_pk=1202, + uuid_value=association_b_uuid, + fields={"name": "B"}, + relations={ + "parentLayer": build_ref( + model="layers.layer", + source_pk=50301, + uuid_value=parent_uuid, + ), + "layer": build_ref( + model="layers.layer", + source_pk=50302, + uuid_value=target_b_uuid, + ), + }, + ), + build_node( + model="layers.multilayerdimensionvalue", + source_pk=1301, + uuid_value=value_1_uuid, + fields={"value": "2020", "label": "2020", "order": 1}, + relations={ + "dimension": build_ref( + model="layers.multilayerdimension", + source_pk=50401, + uuid_value=dimension_uuid, + ), + "associations": [ + build_ref( + model="layers.multilayerassociation", + source_pk=50402, + uuid_value=association_a_uuid, + ) + ], + }, + ), + build_node( + model="layers.multilayerdimensionvalue", + source_pk=1302, + uuid_value=value_2_uuid, + fields={"value": "2021", "label": "2021", "order": 2}, + relations={ + "dimension": build_ref( + model="layers.multilayerdimension", + source_pk=50501, + uuid_value=dimension_uuid, + ), + "associations": [ + build_ref( + model="layers.multilayerassociation", + source_pk=50502, + uuid_value=association_b_uuid, + ) + ], + }, + ), + ] + + import_fixture_rows(fixture_rows, **self._import_kwargs()) + + imported_parent = Layer.objects.get(uuid=parent_uuid) + imported_target_a = Layer.objects.get(uuid=target_a_uuid) + imported_target_b = Layer.objects.get(uuid=target_b_uuid) + + imported_dimension = MultilayerDimension.objects.get(uuid=dimension_uuid) + self.assertEqual(imported_dimension.layer_id, imported_parent.pk) + + association_a = MultilayerAssociation.objects.get(uuid=association_a_uuid) + association_b = MultilayerAssociation.objects.get(uuid=association_b_uuid) + self.assertEqual(association_a.parentLayer_id, imported_parent.pk) + self.assertEqual(association_b.parentLayer_id, imported_parent.pk) + self.assertEqual(association_a.layer_id, imported_target_a.pk) + self.assertEqual(association_b.layer_id, imported_target_b.pk) + + value_1 = MultilayerDimensionValue.objects.get(uuid=value_1_uuid) + value_2 = MultilayerDimensionValue.objects.get(uuid=value_2_uuid) + self.assertEqual(value_1.dimension_id, imported_dimension.pk) + self.assertEqual(value_2.dimension_id, imported_dimension.pk) + self.assertEqual( + set(value_1.associations.values_list("uuid", flat=True)), + {association_a_uuid}, + ) + self.assertEqual( + set(value_2.associations.values_list("uuid", flat=True)), + {association_b_uuid}, + ) + + def test_missing_dimension_or_association_reference_raises_in_strict_mode(self): + self._require_importer() + + value_uuid = uuid4() + missing_dimension_uuid = uuid4() + missing_association_uuid = uuid4() + + fixture_rows = [ + build_node( + model="layers.multilayerdimensionvalue", + source_pk=1401, + uuid_value=value_uuid, + fields={"value": "X", "label": "X", "order": 1}, + relations={ + "dimension": build_ref( + model="layers.multilayerdimension", + source_pk=60101, + uuid_value=missing_dimension_uuid, + ), + "associations": [ + build_ref( + model="layers.multilayerassociation", + source_pk=60102, + uuid_value=missing_association_uuid, + ) + ], + }, + ) + ] + + with self.assertRaises(ValueError): + import_fixture_rows(fixture_rows, **self._import_kwargs()) + def test_second_pass_resolves_specific_layer_rows_by_layer_uuid(self): self._require_importer() From 49df3416e818ee2597d26c618af702fabaac618e Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Thu, 13 Aug 2026 17:35:28 -0700 Subject: [PATCH 2/2] implementing import logic for a layer fixture's slider relations --- layers/fixture_import.py | 67 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/layers/fixture_import.py b/layers/fixture_import.py index 131b44a..042bef9 100644 --- a/layers/fixture_import.py +++ b/layers/fixture_import.py @@ -22,6 +22,8 @@ LAYER_MODEL = "layers.layer" MULTILAYER_ASSOCIATION_MODEL = "layers.multilayerassociation" +MULTILAYER_DIMENSION_MODEL = "layers.multilayerdimension" +MULTILAYER_DIMENSION_VALUE_MODEL = "layers.multilayerdimensionvalue" ATTRIBUTE_INFO_MODEL = "layers.attributeinfo" LOOKUP_INFO_MODEL = "layers.lookupinfo" COMPANIONSHIP_MODEL = "layers.companionship" @@ -137,8 +139,12 @@ def import_fixture_rows( Layer = apps.get_model(LAYER_MODEL) MultilayerAssociation = apps.get_model(MULTILAYER_ASSOCIATION_MODEL) + MultilayerDimension = apps.get_model(MULTILAYER_DIMENSION_MODEL) + MultilayerDimensionValue = apps.get_model(MULTILAYER_DIMENSION_VALUE_MODEL) layer_manager = _model_manager(Layer) association_manager = _model_manager(MultilayerAssociation) + dimension_manager = _model_manager(MultilayerDimension) + dimension_value_manager = _model_manager(MultilayerDimensionValue) def _execute_import(): # First pass: upsert UUID-keyed rows that do not require relation remaps. @@ -213,6 +219,59 @@ def _execute_import(): assoc_obj.layer = layer_obj assoc_obj.save() + # Second pass: upsert multilayer dimensions and resolve owning layer by UUID. + for row in rows: + if row.get(NODE_MODEL_KEY) != MULTILAYER_DIMENSION_MODEL: + continue + + dimension_uuid = normalize_uuid(row.get(NODE_UUID_KEY)) + if not dimension_uuid: + raise ValueError("MultilayerDimension row missing UUID") + + relations = row.get(NODE_RELATIONS_KEY, {}) + layer_ref = relations.get("layer") + if not layer_ref: + raise ValueError("Missing layer relation for MultilayerDimension") + + owner_layer = _resolve_ref_instance(layer_ref, missing_ref_policy) + + dimension_obj = dimension_manager.filter(uuid=dimension_uuid).first() + if dimension_obj is None: + dimension_obj = MultilayerDimension(uuid=dimension_uuid) + + _apply_fields(dimension_obj, row.get(NODE_FIELDS_KEY, {})) + dimension_obj.layer = owner_layer + dimension_obj.save() + + # Second pass: upsert multilayer dimension values and resolve relations by UUID. + for row in rows: + if row.get(NODE_MODEL_KEY) != MULTILAYER_DIMENSION_VALUE_MODEL: + continue + + value_uuid = normalize_uuid(row.get(NODE_UUID_KEY)) + if not value_uuid: + raise ValueError("MultilayerDimensionValue row missing UUID") + + relations = row.get(NODE_RELATIONS_KEY, {}) + dimension_ref = relations.get("dimension") + if not dimension_ref: + raise ValueError("Missing dimension relation for MultilayerDimensionValue") + + dimension_obj = _resolve_ref_instance(dimension_ref, missing_ref_policy) + resolved_associations = _resolve_ref_list( + relations.get("associations") or [], + missing_ref_policy, + ) + + value_obj = dimension_value_manager.filter(uuid=value_uuid).first() + if value_obj is None: + value_obj = MultilayerDimensionValue(uuid=value_uuid) + + _apply_fields(value_obj, row.get(NODE_FIELDS_KEY, {})) + value_obj.dimension = dimension_obj + value_obj.save() + value_obj.associations.set(resolved_associations) + # Second pass: companionship relation rows (non-UUID identity). for row in rows: if row.get(NODE_MODEL_KEY) != COMPANIONSHIP_MODEL: @@ -318,7 +377,13 @@ def import_multilayer_rows( multilayer_rows = [ row for row in (rows or []) - if row.get(NODE_MODEL_KEY) in {LAYER_MODEL, MULTILAYER_ASSOCIATION_MODEL} + if row.get(NODE_MODEL_KEY) + in { + LAYER_MODEL, + MULTILAYER_ASSOCIATION_MODEL, + MULTILAYER_DIMENSION_MODEL, + MULTILAYER_DIMENSION_VALUE_MODEL, + } ] return import_fixture_rows( multilayer_rows,