From b253dadc5759e0882d179cb1a0e51d15ea9e668c Mon Sep 17 00:00:00 2001 From: Riku Oja Date: Fri, 13 Sep 2024 16:54:29 +0300 Subject: [PATCH] Add intended use allocation to database and tests --- database/codes.py | 8 +- ...9_add_separate_fields_for_intended_use_.py | 109 ++++++++++++++++++ database/models.py | 52 ++++++++- database/ryhti_client/ryhti_client.py | 34 +++++- database/test/conftest.py | 28 +++++ database/test/test_koodistot_loader.py | 8 +- database/test/test_models.py | 51 +++++++- database/test/test_ryhti_client.py | 30 +++++ database/test/test_services.py | 18 +++ 9 files changed, 327 insertions(+), 11 deletions(-) create mode 100644 database/migrations/versions/2024_09_13_1603-0d4bf02462e9_add_separate_fields_for_intended_use_.py diff --git a/database/codes.py b/database/codes.py index 03741f3..2c50b2e 100644 --- a/database/codes.py +++ b/database/codes.py @@ -58,10 +58,16 @@ class TypeOfAdditionalInformation(CodeBase): "child_values": [ "paakayttotarkoitus", "osaAlue", - "poisluettavaKayttotarkoitus", "yhteystarve", ], }, + { + "value": "kayttotarkoituskohdistusTaiPoisluettavaKayttotarkoitus", + "name": { + "fin": "Käyttötarkoituskohdistus tai poisluettava käyttötarkoitus" + }, + "child_values": ["kayttotarkoituskohdistus", "poisluettavaKayttotarkoitus"], + }, { "value": "olemassaolo", "name": {"fin": "Olemassaolo"}, diff --git a/database/migrations/versions/2024_09_13_1603-0d4bf02462e9_add_separate_fields_for_intended_use_.py b/database/migrations/versions/2024_09_13_1603-0d4bf02462e9_add_separate_fields_for_intended_use_.py new file mode 100644 index 0000000..7902168 --- /dev/null +++ b/database/migrations/versions/2024_09_13_1603-0d4bf02462e9_add_separate_fields_for_intended_use_.py @@ -0,0 +1,109 @@ +"""add separate fields for intended use allocations + +Revision ID: 0d4bf02462e9 +Revises: b8d3238d6b0a +Create Date: 2024-09-13 16:03:47.595496 + +""" +from typing import Sequence, Union + +import geoalchemy2 +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "0d4bf02462e9" +down_revision: Union[str, None] = "b8d3238d6b0a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "plan_regulation", + sa.Column( + "intended_use_allocation_or_exclusion_id", + sa.UUID(as_uuid=False), + nullable=True, + ), + schema="hame", + ) + op.add_column( + "plan_regulation", + sa.Column( + "first_intended_use_allocation_id", + sa.UUID(as_uuid=False), + nullable=True, + ), + schema="hame", + ) + op.add_column( + "plan_regulation", + sa.Column( + "second_intended_use_allocation_id", + sa.UUID(as_uuid=False), + nullable=True, + ), + schema="hame", + ) + op.create_foreign_key( + "intended_use_allocation_or_exclusion_id_fkey", + "plan_regulation", + "type_of_additional_information", + ["intended_use_allocation_or_exclusion_id"], + ["id"], + source_schema="hame", + referent_schema="codes", + ) + op.create_foreign_key( + "second_intended_use_allocation_id_fkey", + "plan_regulation", + "type_of_plan_regulation", + ["second_intended_use_allocation_id"], + ["id"], + source_schema="hame", + referent_schema="codes", + ) + op.create_foreign_key( + "first_intended_use_allocation_id_fkey", + "plan_regulation", + "type_of_plan_regulation", + ["first_intended_use_allocation_id"], + ["id"], + source_schema="hame", + referent_schema="codes", + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + "first_intended_use_allocation_id_fkey", + "plan_regulation", + schema="hame", + type_="foreignkey", + ) + op.drop_constraint( + "second_intended_use_allocation_id_fkey", + "plan_regulation", + schema="hame", + type_="foreignkey", + ) + op.drop_constraint( + "intended_use_allocation_or_exclusion_id_fkey", + "plan_regulation", + schema="hame", + type_="foreignkey", + ) + op.drop_column( + "plan_regulation", "second_intended_use_allocation_id", schema="hame" + ) + op.drop_column("plan_regulation", "first_intended_use_allocation_id", schema="hame") + op.drop_column( + "plan_regulation", + "intended_use_allocation_or_exclusion_id", + schema="hame", + ) + # ### end Alembic commands ### diff --git a/database/models.py b/database/models.py index 5c38f2b..715ca04 100644 --- a/database/models.py +++ b/database/models.py @@ -187,7 +187,10 @@ class PlanRegulation(PlanBase): ) # Let's load all the codes for objects joined. type_of_plan_regulation = relationship( - "TypeOfPlanRegulation", backref="plan_regulations", lazy="joined" + "TypeOfPlanRegulation", + foreign_keys=[type_of_plan_regulation_id], + backref="plan_regulations", + lazy="joined", ) # Let's load all the codes for objects joined. type_of_verbal_plan_regulation = relationship( @@ -204,6 +207,33 @@ class PlanRegulation(PlanBase): ), nullable=True, ) + # Käyttötarkoituskohdistus/poisluettava käyttötarkoitus + intended_use_allocation_or_exclusion_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey( + "codes.type_of_additional_information.id", + name="intended_use_allocation_or_exclusion_id_fkey", + ), + nullable=True, + ) + # Käyttötarkoituskohdistuksen/poisluettavan käyttötarkoituksen kaavamääräyksen + # tyyppi + # https://koodistot.suomi.fi/code;registryCode=rytj;schemeCode=RY_Kaavamaarayksen_Lisatiedonlaji;codeCode=kayttotarkoituskohdistus + # https://koodistot.suomi.fi/code;registryCode=rytj;schemeCode=RY_Kaavamaarayksen_Lisatiedonlaji;codeCode=poisluettavaKayttotarkoitus + first_intended_use_allocation_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey( + "codes.type_of_plan_regulation.id", + name="first_intended_use_allocation_id_fkey", + ), + nullable=True, + ) + second_intended_use_allocation_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey( + "codes.type_of_plan_regulation.id", + name="second_intended_use_allocation_id_fkey", + ), + nullable=True, + ) + # Olemassaolo existence_id: Mapped[uuid.UUID] = mapped_column( ForeignKey( @@ -267,6 +297,26 @@ class PlanRegulation(PlanBase): backref="intended_use_plan_regulations", lazy="joined", ) + # Let's load all the codes for objects joined. + intended_use_allocation_or_exclusion = relationship( + "TypeOfAdditionalInformation", + foreign_keys=[intended_use_allocation_or_exclusion_id], + backref="intended_use_allocation_plan_regulations", + lazy="joined", + ) + first_intended_use_allocation = relationship( + "TypeOfPlanRegulation", + foreign_keys=[first_intended_use_allocation_id], + backref="first_intended_use_plan_regulations", + lazy="joined", + ) + second_intended_use_allocation = relationship( + "TypeOfPlanRegulation", + foreign_keys=[second_intended_use_allocation_id], + backref="second_intended_use_plan_regulations", + lazy="joined", + ) + existence = relationship( "TypeOfAdditionalInformation", foreign_keys=[existence_id], diff --git a/database/ryhti_client/ryhti_client.py b/database/ryhti_client/ryhti_client.py index 3235298..e1a97ae 100644 --- a/database/ryhti_client/ryhti_client.py +++ b/database/ryhti_client/ryhti_client.py @@ -15,6 +15,7 @@ NameOfPlanCaseDecision, TypeOfDecisionMaker, TypeOfInteractionEvent, + TypeOfPlanRegulation, TypeOfProcessingEvent, decisionmaker_by_status, decisions_by_status, @@ -308,7 +309,7 @@ def get_plan_regulation(self, plan_regulation: models.PlanRegulation) -> Dict: plan_regulation.type_of_verbal_plan_regulation.uri ] regulation_dict["additionalInformations"] = [] - for code_value in [ + for additional_information in [ plan_regulation.intended_use, plan_regulation.existence, plan_regulation.regulation_type_additional_information, @@ -318,10 +319,31 @@ def get_plan_regulation(self, plan_regulation: models.PlanRegulation) -> Dict: plan_regulation.disturbance_prevention, plan_regulation.construction_control, ]: - if code_value: + if additional_information: regulation_dict["additionalInformations"].append( - {"type": code_value.uri} + {"type": additional_information.uri} ) + # Treat intended use allocation separately, it requires extra code values: + if plan_regulation.intended_use_allocation_or_exclusion: + for intended_use_value in ( + plan_regulation.first_intended_use_allocation, + plan_regulation.second_intended_use_allocation, + ): + # if intended_use_value is missing, we cannot just add empty + # additional information + if intended_use_value: + additional_information = { + "type": plan_regulation.intended_use_allocation_or_exclusion.uri + } + additional_information["value"] = dict() + additional_information["value"]["dataType"] = "code" + additional_information["value"]["code"] = intended_use_value.uri + additional_information["value"][ + "codeList" + ] = TypeOfPlanRegulation.code_list_uri + regulation_dict["additionalInformations"].append( + additional_information + ) if plan_regulation.numeric_value: regulation_dict["value"] = { "dataType": "decimal", @@ -329,7 +351,11 @@ def get_plan_regulation(self, plan_regulation: models.PlanRegulation) -> Dict: "number": plan_regulation.numeric_value, "unitOfMeasure": plan_regulation.unit, } - elif plan_regulation.text_value: + elif ( + plan_regulation.text_value.get("fin") + or plan_regulation.text_value.get("swe") + or plan_regulation.text_value.get("eng") + ): regulation_dict["value"] = { "dataType": "LocalizedText", "text": plan_regulation.text_value, diff --git a/database/test/conftest.py b/database/test/conftest.py index d7a6d5b..c86bcf4 100644 --- a/database/test/conftest.py +++ b/database/test/conftest.py @@ -902,6 +902,25 @@ def verbal_plan_regulation_instance( return instance +@pytest.fixture(scope="module") +def intended_use_plan_regulation_instance( + session, + preparation_status_instance, + type_of_plan_regulation_instance, + plan_regulation_group_instance, +): + instance = models.PlanRegulation( + name={"fin": "test_regulation"}, + lifecycle_status=preparation_status_instance, + type_of_plan_regulation=type_of_plan_regulation_instance, + plan_regulation_group=plan_regulation_group_instance, + ordering=4, + ) + session.add(instance) + session.commit() + return instance + + @pytest.fixture(scope="module") def general_plan_regulation_instance( session, @@ -988,10 +1007,12 @@ def complete_test_plan( point_text_plan_regulation_instance: models.PlanRegulation, numeric_plan_regulation_instance: models.PlanRegulation, verbal_plan_regulation_instance: models.PlanRegulation, + intended_use_plan_regulation_instance: models.PlanRegulation, general_plan_regulation_instance: models.PlanRegulation, plan_proposition_instance: models.PlanProposition, plan_theme_instance: codes.PlanTheme, type_of_additional_information_instance: codes.TypeOfAdditionalInformation, + type_of_plan_regulation_instance: codes.TypeOfPlanRegulation, participation_plan_presenting_for_public_decision: codes.NameOfPlanCaseDecision, plan_material_presenting_for_public_decision: codes.NameOfPlanCaseDecision, draft_plan_presenting_for_public_decision: codes.NameOfPlanCaseDecision, @@ -1026,6 +1047,13 @@ def complete_test_plan( verbal_plan_regulation_instance.intended_use = ( type_of_additional_information_instance ) + intended_use_plan_regulation_instance.plan_theme = plan_theme_instance + intended_use_plan_regulation_instance.intended_use_allocation_or_exclusion = ( + type_of_additional_information_instance + ) + intended_use_plan_regulation_instance.first_intended_use_allocation = ( + type_of_plan_regulation_instance + ) general_plan_regulation_instance.plan_theme = plan_theme_instance general_plan_regulation_instance.intended_use = ( type_of_additional_information_instance # noqa diff --git a/database/test/test_koodistot_loader.py b/database/test/test_koodistot_loader.py index 91a4146..3c9a0f0 100644 --- a/database/test/test_koodistot_loader.py +++ b/database/test/test_koodistot_loader.py @@ -406,7 +406,7 @@ def koodistot_data(mock_koodistot, loader): # data should also contain the local codes assert len(data[codes.TypeOfPlanRegulationGroup]) == 5 # for mixed local and remote codes, the data should contain both - assert len(data[codes.TypeOfAdditionalInformation]) == 5 + assert len(data[codes.TypeOfAdditionalInformation]) == 6 return data @@ -422,7 +422,7 @@ def changed_koodistot_data(changed_mock_koodistot, loader): # data should also contain the local codes assert len(data[codes.TypeOfPlanRegulationGroup]) == 5 # for mixed local and remote codes, the data should contain both - assert len(data[codes.TypeOfAdditionalInformation]) == 5 + assert len(data[codes.TypeOfAdditionalInformation]) == 6 return data @@ -590,7 +590,7 @@ def assert_data_is_imported(main_db_params): cur.execute(f"SELECT count(*) FROM codes.type_of_plan_regulation_group") assert cur.fetchone()[0] == 5 cur.execute(f"SELECT count(*) FROM codes.type_of_additional_information") - assert cur.fetchone()[0] == 5 + assert cur.fetchone()[0] == 6 check_code_parents(cur) finally: conn.close() @@ -609,7 +609,7 @@ def assert_changed_data_is_imported(main_db_params): cur.execute(f"SELECT count(*) FROM codes.type_of_plan_regulation_group") assert cur.fetchone()[0] == 5 cur.execute(f"SELECT count(*) FROM codes.type_of_additional_information") - assert cur.fetchone()[0] == 5 + assert cur.fetchone()[0] == 6 check_code_parents(cur) finally: conn.close() diff --git a/database/test/test_models.py b/database/test/test_models.py index f4fd59e..167a724 100644 --- a/database/test/test_models.py +++ b/database/test/test_models.py @@ -202,11 +202,33 @@ def test_plan_regulation( type_of_verbal_plan_regulation_instance ) text_plan_regulation_instance.plan_theme = plan_theme_instance - # All eight additional information regulations are nullable + # All nine additional information regulations (plus two intended use types) are nullable # Käyttötarkoitus assert text_plan_regulation_instance.intended_use is None assert type_of_additional_information_instance.intended_use_plan_regulations == [] text_plan_regulation_instance.intended_use = type_of_additional_information_instance + # Käyttötarkoituskohdistus/poisluettava käyttötarkoitus + assert text_plan_regulation_instance.intended_use_allocation_or_exclusion is None + assert ( + type_of_additional_information_instance.intended_use_allocation_plan_regulations + == [] + ) + text_plan_regulation_instance.intended_use_allocation_or_exclusion = ( + type_of_additional_information_instance + ) + # Käyttötarkoituskohdistuksen/poisluettavan käyttötarkoituksen kaavamääräyksen + # tyyppi + assert text_plan_regulation_instance.first_intended_use_allocation is None + assert text_plan_regulation_instance.second_intended_use_allocation is None + assert type_of_plan_regulation_instance.first_intended_use_plan_regulations == [] + assert type_of_plan_regulation_instance.second_intended_use_plan_regulations == [] + text_plan_regulation_instance.first_intended_use_allocation = ( + type_of_plan_regulation_instance + ) + text_plan_regulation_instance.second_intended_use_allocation = ( + type_of_plan_regulation_instance + ) + # Olemassaolo assert text_plan_regulation_instance.existence is None assert type_of_additional_information_instance.existence_plan_regulations == [] @@ -259,6 +281,7 @@ def test_plan_regulation( ] assert text_plan_regulation_instance.plan_theme is plan_theme_instance assert plan_theme_instance.plan_regulations == [text_plan_regulation_instance] + # Käyttötarkoitus assert ( text_plan_regulation_instance.intended_use @@ -267,6 +290,32 @@ def test_plan_regulation( assert type_of_additional_information_instance.intended_use_plan_regulations == [ text_plan_regulation_instance ] + # Käyttötarkoituskohdistus/poisluettava käyttötarkoitus + assert ( + text_plan_regulation_instance.intended_use_allocation_or_exclusion + is type_of_additional_information_instance + ) + assert ( + type_of_additional_information_instance.intended_use_allocation_plan_regulations + == [text_plan_regulation_instance] + ) + # Käyttötarkoituskohdistuksen/poisluettavan käyttötarkoituksen kaavamääräyksen + # tyyppi + assert ( + text_plan_regulation_instance.first_intended_use_allocation + is type_of_plan_regulation_instance + ) + assert ( + text_plan_regulation_instance.second_intended_use_allocation + is type_of_plan_regulation_instance + ) + assert type_of_plan_regulation_instance.first_intended_use_plan_regulations == [ + text_plan_regulation_instance + ] + assert type_of_plan_regulation_instance.second_intended_use_plan_regulations == [ + text_plan_regulation_instance + ] + # Olemassaolo assert ( text_plan_regulation_instance.existence diff --git a/database/test/test_ryhti_client.py b/database/test/test_ryhti_client.py index bbb129f..2c19324 100644 --- a/database/test/test_ryhti_client.py +++ b/database/test/test_ryhti_client.py @@ -19,6 +19,7 @@ def desired_plan_dict( point_plan_regulation_group_instance: models.PlanRegulationGroup, general_regulation_group_instance: models.PlanRegulationGroup, text_plan_regulation_instance: models.PlanRegulation, + intended_use_plan_regulation_instance: models.PlanRegulation, point_text_plan_regulation_instance: models.PlanRegulation, numeric_plan_regulation_instance: models.PlanRegulation, verbal_plan_regulation_instance: models.PlanRegulation, @@ -273,6 +274,35 @@ def desired_plan_dict( # TODO: plan regulation documents to be added. "periodOfValidity": None, }, + { + "planRegulationKey": intended_use_plan_regulation_instance.id, + "lifeCycleStatus": "http://uri.suomi.fi/codelist/rytj/kaavaelinkaari/code/03", + "type": "http://uri.suomi.fi/codelist/rytj/RY_Kaavamaarayslaji/code/test", + "subjectIdentifiers": [ + intended_use_plan_regulation_instance.name[ + "fin" + ] # TODO: onko asiasana aina yksikielinen?? + ], + "additionalInformations": [ + { + "type": "http://uri.suomi.fi/codelist/rytj/RY_Kaavamaarayksen_Lisatiedonlaji/code/test", + "value": { + "dataType": "code", + "code": "http://uri.suomi.fi/codelist/rytj/RY_Kaavamaarayslaji/code/test", + "codeList": "http://uri.suomi.fi/codelist/rytj/RY_Kaavamaarayslaji", + }, + } + ], + "planThemes": [ + "http://uri.suomi.fi/codelist/rytj/kaavoitusteema/code/test", + ], + # oh great, integer has to be string here for reasons unknown. + "regulationNumber": str( + intended_use_plan_regulation_instance.ordering + ), + # TODO: plan regulation documents to be added. + "periodOfValidity": None, + }, ], "planRecommendations": [ { diff --git a/database/test/test_services.py b/database/test/test_services.py index 98f48db..2e2612b 100644 --- a/database/test/test_services.py +++ b/database/test/test_services.py @@ -254,6 +254,7 @@ def valid_plan_in_preparation( point_text_plan_regulation_instance: models.PlanRegulation, numeric_plan_regulation_instance: models.PlanRegulation, verbal_plan_regulation_instance: models.PlanRegulation, + intended_use_plan_regulation_instance: models.PlanRegulation, general_plan_regulation_instance: models.PlanRegulation, plan_proposition_instance: models.PlanProposition, ): @@ -284,6 +285,7 @@ def valid_plan_in_preparation( numeric_plan_regulation_instance.plan_theme = community_structure_theme verbal_plan_regulation_instance.plan_theme = community_structure_theme general_plan_regulation_instance.plan_theme = community_structure_theme + intended_use_plan_regulation_instance.plan_theme = community_structure_theme # Kaavamääräyksen tyyppi detached_houses_type = ( @@ -301,6 +303,7 @@ def valid_plan_in_preparation( .first() ) verbal_plan_regulation_instance.type_of_plan_regulation = verbal_type + intended_use_plan_regulation_instance.type_of_plan_regulation = detached_houses_type # Sanallisen kaavamääräyksen laji foundation_type_of_verbal_regulation = ( @@ -324,6 +327,21 @@ def valid_plan_in_preparation( numeric_plan_regulation_instance.intended_use = ( principal_intended_use_type_of_additional_information ) + intended_use_plan_regulation_instance.intended_use = ( + principal_intended_use_type_of_additional_information + ) + intended_use_allocation_type_of_additional_information = ( + session.query(codes.TypeOfAdditionalInformation) + .filter_by(value="kayttotarkoitusKohdistus") + .first() + ) + intended_use_plan_regulation_instance.intended_use_allocation_or_exclusion = ( + intended_use_allocation_type_of_additional_information + ) + intended_use_plan_regulation_instance.first_intended_use_allocation = ( + detached_houses_type + ) + # General and verbal regulation type may *not* be intended use regulation! verbal_plan_regulation_instance.intended_use = None general_plan_regulation_instance.intended_use = None