diff --git a/modelbaker/dataobjects/layers.py b/modelbaker/dataobjects/layers.py index 27c4197..349a112 100644 --- a/modelbaker/dataobjects/layers.py +++ b/modelbaker/dataobjects/layers.py @@ -327,9 +327,9 @@ def post_generate(self, project: Project) -> None: continue if nm_relation: - tab = FormTab(nm_relation.referenced_layer.name) + tab = FormTab(nm_relation.referenced_layer.alias) else: - tab = FormTab(relation.referencing_layer.name) + tab = FormTab(relation.referencing_layer.alias) widget = FormRelationWidget(relation, nm_relation) tab.addChild(widget) diff --git a/modelbaker/dataobjects/relations.py b/modelbaker/dataobjects/relations.py index 91bbaf2..86cc68d 100644 --- a/modelbaker/dataobjects/relations.py +++ b/modelbaker/dataobjects/relations.py @@ -15,6 +15,7 @@ def __init__(self) -> None: self.child_domain_name = None self.qgis_relation = None self._id = None + self.translate_name = False def dump(self) -> dict: definition = dict() @@ -57,10 +58,31 @@ def create( relation.setId(self._id) relation.setName(self.name) - relation.setReferencingLayer(self.referencing_layer.create().id()) - relation.setReferencedLayer(self.referenced_layer.create().id()) + referencing_qgis_layer = self.referencing_layer.create() + referenced_qgis_layer = self.referenced_layer.create() + relation.setReferencingLayer(referencing_qgis_layer.id()) + relation.setReferencedLayer(referenced_qgis_layer.id()) relation.addFieldPair(self.referencing_field, self.referenced_field) relation.setStrength(self.strength) + + if self.translate_name: + # Grab translated table and field names from QGIS objects + index = referencing_qgis_layer.fields().indexOf(self.referencing_field) + referencing_field_alias = referencing_qgis_layer.fields().at(index).alias() + index = referenced_qgis_layer.fields().indexOf(self.referenced_field) + referenced_field_alias = referenced_qgis_layer.fields().at(index).alias() + tr_name = "{}_({})_{}_({})".format( + referencing_qgis_layer.name(), + referencing_field_alias + if referencing_field_alias + else self.referencing_field, + referenced_qgis_layer.name(), + referenced_field_alias + if referenced_field_alias + else self.referenced_field, + ) + relation.setName(tr_name) + self.qgis_relation = relation return relation diff --git a/modelbaker/dbconnector/config.py b/modelbaker/dbconnector/config.py index ee1c215..a9b9082 100644 --- a/modelbaker/dbconnector/config.py +++ b/modelbaker/dbconnector/config.py @@ -49,6 +49,8 @@ "T_ILI2DB_BASKET", "t_ili2db_dataset", "T_ILI2DB_DATASET", + "t_ili2db_nls", + "T_ILI2DB_NLS", ] BASKET_TABLES = [ diff --git a/modelbaker/dbconnector/db_connector.py b/modelbaker/dbconnector/db_connector.py index e1ebbfc..199a68a 100644 --- a/modelbaker/dbconnector/db_connector.py +++ b/modelbaker/dbconnector/db_connector.py @@ -15,7 +15,6 @@ * * ***************************************************************************/ """ - from qgis.PyQt.QtCore import QObject, pyqtSignal from .config import BASKET_TABLES, IGNORED_ILI_ELEMENTS, IGNORED_SCHEMAS, IGNORED_TABLES @@ -38,6 +37,7 @@ def __init__(self, uri, schema, parent=None): self.dispName = "" # For BAG OF config, specific for each DB self.basket_table_name = "" # For basket handling, specific for each DB self.dataset_table_name = "" # For basket handling, specific for each DB + self._lang = "" # Preferred tr language for table/column info (2 characters) def map_data_types(self, data_type): """Map provider date/time types to QGIS date/time types""" @@ -404,6 +404,35 @@ def set_ili2db_sequence_value(self, value): """ return False, None + def set_preferred_translation(self, lang: str) -> bool: + """ + Returns whether the preferred translation language was successfully set. + + Note: By convention, a value of __ means the preferred language will be + the original (non-translated) model language. + """ + if len(lang) == 2 and lang != "__": + self._lang = lang + return True + + return False + + def get_translation_handling(self) -> tuple[bool, str]: + """ + Whether there is translation support for this DB. + + :return: Tuple containing: + - Whether the t_ili2db_nls is present and the DB connector has a preferred language set. + - The preferred language set. + """ + return False, "" + + def get_available_languages(self) -> list[str]: + """ + Returns a list of available languages in the t_ili2db_nls table. + """ + return [] + class DBConnectorError(Exception): """This error is raised when DbConnector could not connect to database. diff --git a/modelbaker/dbconnector/gpkg_connector.py b/modelbaker/dbconnector/gpkg_connector.py index 6c1f448..3eb0c4a 100644 --- a/modelbaker/dbconnector/gpkg_connector.py +++ b/modelbaker/dbconnector/gpkg_connector.py @@ -32,6 +32,7 @@ GPKG_SETTINGS_TABLE = "T_ILI2DB_SETTINGS" GPKG_DATASET_TABLE = "T_ILI2DB_DATASET" GPKG_BASKET_TABLE = "T_ILI2DB_BASKET" +GPKG_NLS_TABLE = "T_ILI2DB_NLS" class GPKGConnector(DBConnector): @@ -49,7 +50,7 @@ def __init__(self, uri, schema): self.conn.row_factory = sqlite3.Row self.uri = uri self._bMetadataTable = self._metadata_exists() - self._tables_info = self._get_tables_info() + self._tables_info = [] self.iliCodeName = "iliCode" self.tid = "T_Id" self.tilitid = "T_Ili_Tid" @@ -97,6 +98,8 @@ def _table_exists(self, tablename): return result def get_tables_info(self): + if not self._tables_info: + self._tables_info = self._get_tables_info() return self._tables_info def _get_tables_info(self): @@ -105,6 +108,12 @@ def _get_tables_info(self): interlis_joins = "" if self.metadata_exists(): + tr_enabled, lang = self.get_translation_handling() + if tr_enabled: + self.stdout.emit( + f"Getting tables info with preferred language '{lang}'." + ) + interlis_fields = """p.setting AS kind_settings, alias.setting AS table_alias, c.iliname AS ili_name, @@ -139,7 +148,9 @@ def _get_tables_info(self): substr(c.iliname, 0, instr(c.iliname, '.')) AS model, attrs.sqlname as attribute_name, {relevance_field}, - {topics},""".format( + {topics}, + {translations} -- Optional. Trailing comma omitted on purpose. + """.format( relevance_field="""CASE WHEN c.iliname IN ( -- used to get the class names from the full names WITH names AS ( @@ -208,6 +219,7 @@ def _get_tables_info(self): ) SELECT childTopic, baseTopic, is_a_base FROM children)""" ), + translations="""nls.label AS table_tr,""" if tr_enabled else "", ) interlis_joins = """LEFT JOIN T_ILI2DB_TABLE_PROP p ON p.tablename = s.name @@ -218,7 +230,15 @@ def _get_tables_info(self): LEFT JOIN T_ILI2DB_CLASSNAME c ON s.name == c.sqlname LEFT JOIN T_ILI2DB_ATTRNAME attrs - ON c.iliname = attrs.iliname """ + ON c.iliname = attrs.iliname + {translations}""".format( + translations=f"""LEFT JOIN T_ILI2DB_NLS nls + ON c.iliname = nls.ilielement + AND nls.lang = '{lang}' + """ + if tr_enabled + else "" + ) try: cursor.execute( """ @@ -331,6 +351,8 @@ def get_fields_info(self, table_name): columns_prop = list() columns_full_name = list() meta_attrs = list() + columns_tr = list() + tr_enabled, lang = self.get_translation_handling() if self.metadata_exists(): cursor.execute( @@ -342,7 +364,6 @@ def get_fields_info(self, table_name): ) columns_prop = cursor.fetchall() - if self.metadata_exists(): if self.ili_version() == 3: cursor.execute( """ @@ -361,9 +382,20 @@ def get_fields_info(self, table_name): ) columns_full_name = cursor.fetchall() - if self.metadata_exists() and self._table_exists(GPKG_METAATTRS_TABLE): - meta_attrs = self.get_meta_attrs_info() + if self._table_exists(GPKG_METAATTRS_TABLE): + meta_attrs = self.get_meta_attrs_info() + if tr_enabled: + cursor.execute( + """ + SELECT ilielement, label + FROM T_ILI2DB_NLS + WHERE lang = ?;""", + (lang,), + ) + columns_tr = cursor.fetchall() + + # Build result dict from query results complete_records = list() for column_info in columns_info: record = {} @@ -385,6 +417,11 @@ def get_fields_info(self, table_name): for column_full_name in columns_full_name: if column_full_name["sqlname"] == column_info["name"]: record["fully_qualified_name"] = column_full_name["iliname"] + + for column_tr in columns_tr: + if column_full_name["iliname"] == column_tr["ilielement"]: + record["column_tr"] = column_tr["label"] + break break for column_prop in columns_prop: @@ -486,6 +523,8 @@ def get_relations_info(self, filter_layer_list=[]): cursor = self.conn.cursor() complete_records = list() + tr_enabled, lang = self.get_translation_handling() + for table_info_name, table_info in tables_info_dict.items(): cursor.execute("""PRAGMA foreign_key_list("{}");""".format(table_info_name)) foreign_keys = cursor.fetchall() @@ -504,6 +543,9 @@ def get_relations_info(self, filter_layer_list=[]): record["referenced_table"], record["referenced_column"], ) + if tr_enabled: + record["tr_enabled"] = True + if self._table_exists(GPKG_METAATTRS_TABLE): # Get strength cursor.execute( @@ -1189,3 +1231,23 @@ def set_ili2db_sequence_value(self, value): ) return False, self.tr("Could not reset T_LastUniqueId") + + def get_translation_handling(self) -> tuple[bool, str]: + return self._table_exists(GPKG_NLS_TABLE) and self._lang != "", self._lang + + def get_available_languages(self): + if not self._table_exists(GPKG_NLS_TABLE): + return [] + + cursor = self.conn.cursor() + cursor.execute( + """SELECT DISTINCT + lang + FROM "{}"; + """.format( + GPKG_NLS_TABLE + ) + ) + records = cursor.fetchall() + cursor.close() + return [record["lang"] for record in records] diff --git a/modelbaker/dbconnector/mssql_connector.py b/modelbaker/dbconnector/mssql_connector.py index 713c7ce..1c18984 100644 --- a/modelbaker/dbconnector/mssql_connector.py +++ b/modelbaker/dbconnector/mssql_connector.py @@ -29,6 +29,7 @@ SETTINGS_TABLE = "t_ili2db_settings" DATASET_TABLE = "T_ILI2DB_DATASET" BASKET_TABLE = "T_ILI2DB_BASKET" +NLS_TABLE = "t_ili2db_nls" class MssqlConnector(DBConnector): @@ -114,6 +115,7 @@ def get_tables_info(self): if self.schema: metadata_exists = self.metadata_exists() + tr_enabled, lang = self.get_translation_handling() ln = "\n" stmt = "" @@ -231,6 +233,8 @@ def get_tables_info(self): + """ ,substring( c.iliname, 1, CHARINDEX('.', substring( c.iliname, CHARINDEX('.', c.iliname)+1, len(c.iliname)))+CHARINDEX('.', c.iliname)-1) as base_topic """ ) + if tr_enabled: + stmt += ln + " , nls.label AS table_tr" stmt += ln + "FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS Tab" stmt += ln + "INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS Col" stmt += ln + " ON Col.Constraint_Name = Tab.Constraint_Name" @@ -264,6 +268,10 @@ def get_tables_info(self): stmt += ln + " ON tbls.TABLE_NAME = tgeomtype.tablename" stmt += ln + " AND clm.COLUMN_NAME = tgeomtype.columnname" stmt += ln + " AND tgeomtype.tag= 'ch.ehi.ili2db.geomType'" + if tr_enabled: + stmt += ln + "LEFT JOIN {schema}.t_ili2db_nls nls" + stmt += ln + " ON c.iliname = nls.ilielement" + stmt += ln + " AND nls.lang = '{lang}'".format(lang=lang) stmt += ( ln + "WHERE tbls.TABLE_TYPE = 'BASE TABLE' AND tbls.TABLE_SCHEMA = '{schema}'" @@ -481,6 +489,7 @@ def get_fields_info(self, table_name): if self.schema: metadata_exists = self.metadata_exists() metaattrs_exists = self._table_exists(METAATTRS_TABLE) + tr_enabled, lang = self.get_translation_handling() ln = "\n" stmt = "" @@ -506,6 +515,8 @@ def get_fields_info(self, table_name): + " , attr_mapping.attr_value AS attr_mapping" ) stmt += ln + " , null AS comment" + if tr_enabled: + stmt += ln + " , nls.label AS column_tr" stmt += ln + "FROM INFORMATION_SCHEMA.COLUMNS AS c" if metadata_exists: stmt += ln + "LEFT JOIN {schema}.t_ili2db_column_prop unit" @@ -545,6 +556,10 @@ def get_fields_info(self, table_name): stmt += ln + "LEFT JOIN {schema}.t_ili2db_meta_attrs attr_mapping" stmt += ln + " ON full_name.iliname=attr_mapping.ilielement AND" stmt += ln + " attr_mapping.attr_name='ili2db.mapping'" + if tr_enabled: + stmt += ln + "LEFT JOIN {schema}.t_ili2db_nls nls" + stmt += ln + " ON full_name.iliname = nls.ilielement" + stmt += ln + " AND nls.lang = '{lang}'".format(lang=lang) stmt += ln + "WHERE TABLE_NAME = '{table}' AND TABLE_SCHEMA = '{schema}'" if metadata_exists and metaattrs_exists: stmt += ln + "ORDER BY attr_order;" @@ -625,6 +640,9 @@ def get_relations_info(self, filter_layer_list=[]): if self.schema: cur = self.conn.cursor() + translate = ( + ", 1 AS tr_enabled" if self.get_translation_handling()[0] else "" + ) schema_where1 = ( "AND KCU1.CONSTRAINT_SCHEMA = '{}'".format(self.schema) if self.schema @@ -681,6 +699,7 @@ def get_relations_info(self, filter_layer_list=[]): ,KCU1.ORDINAL_POSITION AS ordinal_position {strength_field} {cardinality_max_field} + {translate} FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU1 @@ -706,6 +725,7 @@ def get_relations_info(self, filter_layer_list=[]): strength_join=strength_join, cardinality_max_field=cardinality_max_field, cardinality_max_join=cardinality_max_join, + translate=translate, ) cur.execute(query) result = self._get_dict_result(cur) @@ -1222,3 +1242,20 @@ def set_ili2db_sequence_value(self, value): ) return False, self.tr("Could not reset sequence") + + def get_translation_handling(self) -> tuple[bool, str]: + return self._table_exists(NLS_TABLE) and self._lang != "", self._lang + + def get_available_languages(self): + if self.schema and self._table_exists(NLS_TABLE): + cur = self.conn.cursor() + cur.execute( + """ + SELECT DISTINCT + lang + FROM {schema}.t_ili2db_nls + """ + ).format(schema=self.schema) + + return [row.lang for row in cur.fetchall()] + return [] diff --git a/modelbaker/dbconnector/pg_connector.py b/modelbaker/dbconnector/pg_connector.py index eebc15a..fa6d4da 100644 --- a/modelbaker/dbconnector/pg_connector.py +++ b/modelbaker/dbconnector/pg_connector.py @@ -31,6 +31,7 @@ PG_SETTINGS_TABLE = "t_ili2db_settings" PG_DATASET_TABLE = "t_ili2db_dataset" PG_BASKET_TABLE = "t_ili2db_basket" +PG_NLS_TABLE = "t_ili2db_nls" class PGConnector(DBConnector): @@ -160,10 +161,18 @@ def get_tables_info(self): model_where = "" attribute_name = "" attribute_left_join = "" + translations_left_join = "" relevance = "" topics = "" + translations = "" if self.metadata_exists(): + tr_enabled, lang = self.get_translation_handling() + if tr_enabled: + self.stdout.emit( + f"Getting tables info with preferred language '{lang}'." + ) + kind_settings_field = "p.setting AS kind_settings," table_alias = "alias.setting AS table_alias," ili_name = "c.iliname AS ili_name," @@ -265,6 +274,8 @@ def get_tables_info(self): ) ) + translations = """nls.label AS table_tr,""" if tr_enabled else "" + domain_left_join = """LEFT JOIN {}.t_ili2db_table_prop p ON p.tablename = tbls.tablename AND p.tag = 'ch.ehi.ili2db.tableKind'""".format( @@ -284,6 +295,16 @@ def get_tables_info(self): ON c.iliname = attrs.iliname""".format( self.schema ) + translations_left_join = ( + """LEFT JOIN {}.t_ili2db_nls nls + ON c.iliname = nls.ilielement + AND nls.lang = '{}' + """.format( + self.schema, lang + ) + if tr_enabled + else "" + ) schema_where = "AND schemaname = '{}'".format(self.schema) @@ -306,6 +327,7 @@ def get_tables_info(self): {coord_decimals} {relevance} {topics} + {translations} g.type AS simple_type, format_type(ga.atttypid, ga.atttypmod) as formatted_type FROM pg_catalog.pg_tables tbls @@ -318,6 +340,7 @@ def get_tables_info(self): {alias_left_join} {model_where} {attribute_left_join} + {translations_left_join} LEFT JOIN public.geometry_columns g ON g.f_table_schema = tbls.schemaname AND g.f_table_name = tbls.tablename @@ -334,8 +357,10 @@ def get_tables_info(self): coord_decimals=coord_decimals, relevance=relevance, topics=topics, + translations=translations, domain_left_join=domain_left_join, alias_left_join=alias_left_join, + translations_left_join=translations_left_join, model_where=model_where, attribute_name=attribute_name, attribute_left_join=attribute_left_join, @@ -430,6 +455,7 @@ def get_fields_info(self, table_name): oid_domain_field = "" attr_order_field = "" attr_mapping_field = "" + translations = "" column_alias = "" unit_join = "" text_kind_join = "" @@ -439,15 +465,19 @@ def get_fields_info(self, table_name): oid_domain_join = "" attr_order_join = "" attr_mapping_join = "" + translations_left_join = "" order_by_attr_order = "" if self.metadata_exists(): + tr_enabled, lang = self.get_translation_handling() + unit_field = "unit.setting AS unit," text_kind_field = "txttype.setting AS texttype," column_alias = "alias.setting AS column_alias," full_name_field = "full_name.iliname as fully_qualified_name," enum_domain_field = "enum_domain.setting as enum_domain," oid_domain_field = "oid_domain.setting as oid_domain," + translations = """nls.label AS column_tr,""" if tr_enabled else "" unit_join = """LEFT JOIN {}.t_ili2db_column_prop unit ON c.table_name=unit.tablename AND c.column_name=unit.columnname AND @@ -486,6 +516,7 @@ def get_fields_info(self, table_name): oid_domain.tag = 'ch.ehi.ili2db.oidDomain'""".format( self.schema ) + if self._table_exists(PG_METAATTRS_TABLE): attr_order_field = "COALESCE(to_number(form_order.attr_value, '999'), 999) as attr_order," attr_order_join = """LEFT JOIN {schema}.{t_ili2db_meta_attrs} form_order @@ -509,6 +540,17 @@ def get_fields_info(self, table_name): schema=self.schema, t_ili2db_meta_attrs=PG_METAATTRS_TABLE ) + translations_left_join = ( + """LEFT JOIN {}.t_ili2db_nls nls + ON full_name.iliname = nls.ilielement + AND nls.lang = '{}' + """.format( + self.schema, lang + ) + if tr_enabled + else "" + ) + fields_cur.execute( """ SELECT @@ -523,6 +565,7 @@ def get_fields_info(self, table_name): {oid_domain_field} {attr_order_field} {attr_mapping_field} + {translations} pgd.description AS comment FROM pg_catalog.pg_statio_all_tables st LEFT JOIN information_schema.columns c ON c.table_schema=st.schemaname AND c.table_name=st.relname @@ -535,6 +578,7 @@ def get_fields_info(self, table_name): {oid_domain_join} {attr_order_join} {attr_mapping_join} + {translations_left_join} WHERE st.relid = '{schema}."{table}"'::regclass {order_by_attr_order}; """.format( @@ -548,6 +592,7 @@ def get_fields_info(self, table_name): oid_domain_field=oid_domain_field, attr_order_field=attr_order_field, attr_mapping_field=attr_mapping_field, + translations=translations, unit_join=unit_join, text_kind_join=text_kind_join, disp_name_join=disp_name_join, @@ -556,6 +601,7 @@ def get_fields_info(self, table_name): oid_domain_join=oid_domain_join, attr_order_join=attr_order_join, attr_mapping_join=attr_mapping_join, + translations_left_join=translations_left_join, order_by_attr_order=order_by_attr_order, ) ) @@ -669,6 +715,8 @@ def get_relations_info(self, filter_layer_list=[]): cardinality_max_field = "" cardinality_max_join = "" cardinality_max_group_by = "" + translate = "" + if self._table_exists(PG_METAATTRS_TABLE): strength_field = ", META_ATTRS.attr_value as strength" strength_join = """ @@ -696,8 +744,12 @@ def get_relations_info(self, filter_layer_list=[]): ) cardinality_max_group_by = ", META_ATTRS_CARDINALITY.attr_value" + translate = ( + ", true AS tr_enabled" if self.get_translation_handling()[0] else "" + ) + cur.execute( - """SELECT RC.CONSTRAINT_NAME, KCU1.TABLE_NAME AS referencing_table, KCU1.COLUMN_NAME AS referencing_column, KCU2.CONSTRAINT_SCHEMA, KCU2.TABLE_NAME AS referenced_table, KCU2.COLUMN_NAME AS referenced_column, KCU1.ORDINAL_POSITION{strength_field}{cardinality_max_field} + """SELECT RC.CONSTRAINT_NAME, KCU1.TABLE_NAME AS referencing_table, KCU1.COLUMN_NAME AS referencing_column, KCU2.CONSTRAINT_SCHEMA, KCU2.TABLE_NAME AS referenced_table, KCU2.COLUMN_NAME AS referenced_column, KCU1.ORDINAL_POSITION{strength_field}{cardinality_max_field}{translate} FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU1 ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME {schema_where1} {filter_layer_where} @@ -718,6 +770,7 @@ def get_relations_info(self, filter_layer_list=[]): cardinality_max_field=cardinality_max_field, cardinality_max_join=cardinality_max_join, cardinality_max_group_by=cardinality_max_group_by, + translate=translate, ) ) return cur @@ -1314,3 +1367,21 @@ def get_schemas(self): # Transform list of tuples into list return list(sum(schemas, ())) + + def get_translation_handling(self) -> tuple[bool, str]: + return self._table_exists(PG_NLS_TABLE) and self._lang != "", self._lang + + def get_available_languages(self): + if self.schema and self._table_exists(PG_NLS_TABLE): + cur = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + cur.execute( + sql.SQL( + """ + SELECT DISTINCT + lang + FROM {schema}.t_ili2db_nls + """ + ).format(schema=sql.Identifier(self.schema)) + ) + return [row["lang"] for row in cur.fetchall()] + return [] diff --git a/modelbaker/generator/generator.py b/modelbaker/generator/generator.py index d98b983..fe828ca 100644 --- a/modelbaker/generator/generator.py +++ b/modelbaker/generator/generator.py @@ -53,6 +53,7 @@ def __init__( mgmt_uri: Optional[str] = None, consider_basket_handling: bool = False, optimize_strategy: OptimizeStrategy = OptimizeStrategy.NONE, + preferred_language: str = "", ) -> None: """ Creates a new Generator objects. @@ -76,6 +77,8 @@ def __init__( self.basket_handling = consider_basket_handling and self.get_basket_handling() self.optimize_strategy = optimize_strategy + self._db_connector.set_preferred_translation(preferred_language) + self._additional_ignored_layers = ( [] ) # List of layers to ignore set by 3rd parties @@ -177,7 +180,12 @@ def layers(self, filter_layer_list: list = []) -> list[Layer]: if not relevant_topics and base_topic and base_topic.count(".") > 0: relevant_topics.append(base_topic) - alias = record["table_alias"] if "table_alias" in record else None + # Get table name in this order: translation if exists, + # alias (dispName) if exists, or ili_name. + alias = record.get("table_tr", None) + if not alias: + alias = record.get("table_alias", None) + if not alias: short_name = None if is_domain and is_attribute: @@ -288,22 +296,26 @@ def layers(self, filter_layer_list: list = []) -> list[Layer]: re_iliname = re.compile(r".*\.(.*)$") for fielddef in fields_info: column_name = fielddef["column_name"] - fully_qualified_name = ( - fielddef["fully_qualified_name"] - if "fully_qualified_name" in fielddef - else None - ) - m = ( - re_iliname.match(fully_qualified_name) - if fully_qualified_name - else None - ) - alias = None - if "column_alias" in fielddef: - alias = fielddef["column_alias"] - if m and not alias: - alias = m.group(1) + # Get field name in this order: translation if exists, + # alias (dispName) if exists, or ili_name. + alias = fielddef.get("column_tr", None) + if not alias: + alias = fielddef.get("column_alias", None) + + if not alias: + fully_qualified_name = ( + fielddef["fully_qualified_name"] + if "fully_qualified_name" in fielddef + else None + ) + m = ( + re_iliname.match(fully_qualified_name) + if fully_qualified_name + else None + ) + if m: + alias = m.group(1) field = Field(column_name) field.alias = alias @@ -517,6 +529,7 @@ def relations(self, layers, filter_layer_list=[]): relation.referencing_field = record["referencing_column"] relation.referenced_field = record["referenced_column"] relation.name = record["constraint_name"] + relation.translate_name = record.get("tr_enabled", False) relation.strength = ( QgsRelation.Composition if "strength" in record diff --git a/modelbaker/iliwrapper/ili2dbconfig.py b/modelbaker/iliwrapper/ili2dbconfig.py index 28c1e0e..d04aa19 100644 --- a/modelbaker/iliwrapper/ili2dbconfig.py +++ b/modelbaker/iliwrapper/ili2dbconfig.py @@ -324,6 +324,9 @@ def to_ili2db_args(self, extra_args=[], with_action=True): elif self.db_ili_version is None or self.db_ili_version > 3: self.append_args(args, ["--postScript", "NULL"]) + if self.db_ili_version is None or self.db_ili_version > 3: + self.append_args(args, ["--createNlsTab"]) + self.append_args( args, Ili2DbCommandConfiguration.to_ili2db_args(self), force_append=True ) diff --git a/modelbaker/iliwrapper/ili2dbtools.py b/modelbaker/iliwrapper/ili2dbtools.py index 68d6e13..84aca69 100644 --- a/modelbaker/iliwrapper/ili2dbtools.py +++ b/modelbaker/iliwrapper/ili2dbtools.py @@ -25,17 +25,17 @@ def get_tool_version(tool, db_ili_version): if db_ili_version == 3: return "3.11.3" else: - return "5.1.1" + return "5.2.0" elif tool == DbIliMode.ili2pg: if db_ili_version == 3: return "3.11.2" else: - return "5.1.1" + return "5.2.0" elif tool == DbIliMode.ili2mssql: if db_ili_version == 3: return "3.12.2" else: - return "5.1.1" + return "5.2.0" return "0" diff --git a/modelbaker/iliwrapper/iliexecutable.py b/modelbaker/iliwrapper/iliexecutable.py index 50b6c08..92feec7 100644 --- a/modelbaker/iliwrapper/iliexecutable.py +++ b/modelbaker/iliwrapper/iliexecutable.py @@ -96,6 +96,9 @@ def _escaped_arg(self, argument): def command(self, hide_password): ili2db_jar_arg = self._ili2db_jar_arg() + if ili2db_jar_arg == self.ILI2DB_NOT_FOUND: + return "ili2db tool not found!" + args = self._args(hide_password) java_path = self._escaped_arg( get_java_path(self.configuration.base_configuration) diff --git a/tests/test_projectgen.py b/tests/test_projectgen.py index 553ecd2..661ff50 100644 --- a/tests/test_projectgen.py +++ b/tests/test_projectgen.py @@ -127,10 +127,10 @@ def test_ili2db3_kbs_postgis(self): tab_list = [tab.name() for tab in tabs] expected_tab_list = [ "General", - "parzellenidentifikation", - "egrid_", - "deponietyp", - "untersmassn", + "Parzellenidentifikation", + "EGRID_", + "Deponietyp", + "UntersMassn", ] assert set(tab_list) == set(expected_tab_list) assert len(tab_list) == len(expected_tab_list) @@ -246,10 +246,10 @@ def test_kbs_postgis(self): tab_list = [tab.name() for tab in tabs] expected_tab_list = [ "General", - "parzellenidentifikation", - "egrid_", - "deponietyp", - "untersmassn", + "Parzellenidentifikation", + "EGRID_", + "Deponietyp", + "UntersMassn", ] assert len(tab_list) == len(expected_tab_list) assert set(tab_list) == set(expected_tab_list) @@ -363,11 +363,11 @@ def test_ili2db3_kbs_geopackage(self): tab_list = [tab.name() for tab in tabs] expected_tab_list = [ "General", - "parzellenidentifikation", - "belasteter_standort_geo_lage_punkt", - "egrid_", - "deponietyp", - "untersmassn", + "Parzellenidentifikation", + "Geo_Lage_Punkt", + "EGRID_", + "Deponietyp", + "UntersMassn", ] assert len(tab_list) == len(expected_tab_list) assert set(tab_list) == set(expected_tab_list) @@ -464,11 +464,11 @@ def test_kbs_geopackage(self): tab_list = [tab.name() for tab in tabs] expected_tab_list = [ "General", - "parzellenidentifikation", - "belasteter_standort_geo_lage_punkt", - "egrid_", - "deponietyp", - "untersmassn", + "Parzellenidentifikation", + "Belasteter_Standort (Geo_Lage_Punkt)", + "EGRID_", + "Deponietyp", + "UntersMassn", ] assert len(tab_list) == len(expected_tab_list) assert set(tab_list) == set(expected_tab_list) @@ -557,7 +557,7 @@ def test_naturschutz_geopackage(self): available_layers = generator.layers([]) relations, _ = generator.relations(available_layers) - assert len(ignored_layers) == 64 + assert len(ignored_layers) == 65 assert len(available_layers) == 23 assert len(relations) == 13 @@ -593,7 +593,7 @@ def test_naturschutz_mssql(self): available_layers = generator.layers([]) relations, _ = generator.relations(available_layers) - assert len(ignored_layers) == 19 + assert len(ignored_layers) == 20 assert len(available_layers) == 23 assert len(relations) == 22 @@ -658,7 +658,7 @@ def test_naturschutz_set_ignored_layers_geopackage(self): legend = generator.legend(available_layers) relations, _ = generator.relations(available_layers) - assert len(ignored_layers) == 66 + assert len(ignored_layers) == 67 assert len(available_layers) == 21 assert len(relations) == 12 @@ -709,7 +709,7 @@ def test_naturschutz_set_ignored_layers_mssql(self): available_layers = generator.layers([]) relations, _ = generator.relations(available_layers) - assert len(ignored_layers) == 21 + assert len(ignored_layers) == 22 assert len(available_layers) == 21 assert len(relations) == 21 @@ -773,7 +773,7 @@ def test_naturschutz_nometa_geopackage(self): available_layers = generator.layers([]) relations, _ = generator.relations(available_layers) - assert len(ignored_layers) == 30 + assert len(ignored_layers) == 31 assert len(available_layers) == 29 assert len(relations) == 23 @@ -2502,13 +2502,13 @@ def test_relation_cardinality_postgis(self): tab_job = None efc = contact_layer.layer.editFormConfig() for tab in efc.tabs(): - if tab.name() == "address": + if tab.name() == "Address": tab_address = tab - elif tab.name() == "identificator": + elif tab.name() == "Identificator": tab_identificator = tab - elif tab.name() == "ahvnr": + elif tab.name() == "AHVNr": tab_ahvnr = tab - elif tab.name() == "job": + elif tab.name() == "Job": tab_job = tab assert tab_address assert tab_identificator @@ -2591,13 +2591,13 @@ def test_relation_cardinality_geopackage(self): tab_job = None efc = contact_layer.layer.editFormConfig() for tab in efc.tabs(): - if tab.name() == "address": + if tab.name() == "Address": tab_address = tab - elif tab.name() == "identificator": + elif tab.name() == "Identificator": tab_identificator = tab - elif tab.name() == "ahvnr": + elif tab.name() == "AHVNr": tab_ahvnr = tab - elif tab.name() == "job": + elif tab.name() == "Job": tab_job = tab assert tab_address assert tab_identificator @@ -2682,13 +2682,13 @@ def test_relation_cardinality_mssql(self): tab_job = None efc = contact_layer.layer.editFormConfig() for tab in efc.tabs(): - if tab.name() == "address": + if tab.name() == "Address": tab_address = tab - elif tab.name() == "identificator": + elif tab.name() == "Identificator": tab_identificator = tab - elif tab.name() == "ahvnr": + elif tab.name() == "AHVNr": tab_ahvnr = tab - elif tab.name() == "job": + elif tab.name() == "Job": tab_job = tab assert tab_address assert tab_identificator @@ -4206,7 +4206,7 @@ def test_relation_editor_widget_for_no_geometry_layers(self): tab_list = [tab.name() for tab in tabs] expected_tab_list = [ "General", - "maphieritem", + "MapHierItem", ] assert set(tab_list) == set(expected_tab_list) assert len(tab_list) == len(expected_tab_list) diff --git a/tests/test_projectgen_extension_optimization_smart1.py b/tests/test_projectgen_extension_optimization_smart1.py index 056ec53..f9875db 100644 --- a/tests/test_projectgen_extension_optimization_smart1.py +++ b/tests/test_projectgen_extension_optimization_smart1.py @@ -318,7 +318,7 @@ def _extopt_staedtische(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 2 for tab in efc.tabs(): - if tab.name() == "gebaeude": + if tab.name() == "Gebaeude": count += 1 assert len(tab.children()) == 1 # should find 1 @@ -739,7 +739,7 @@ def _extopt_polymorphic(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 2 for tab in efc.tabs(): - if tab.name() == "gebaeude": + if tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude": count += 1 assert len(tab.children()) == 1 # should find 1 @@ -1165,7 +1165,7 @@ def _extopt_baustruct(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 2 for tab in efc.tabs(): - if tab.name() == "strassen_gebaeude": + if tab.name() == "Strassen_Gebaeude": count += 1 assert len(tab.children()) == 1 # should find 1 (one times gebaeude) diff --git a/tests/test_projectgen_extension_optimization_smart2.py b/tests/test_projectgen_extension_optimization_smart2.py index 0a7031a..21accc8 100644 --- a/tests/test_projectgen_extension_optimization_smart2.py +++ b/tests/test_projectgen_extension_optimization_smart2.py @@ -386,16 +386,19 @@ def _extopt_staedtische_none(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 5 for tab in efc.tabs(): - if tab.name() == "stadtscng_v1_1freizeit_gebaeude": + if tab.name() == "Freizeit.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "stadtscng_v1_1gewerbe_gebaeude": + if tab.name() == "Gewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "kantnl_ng_v1_1konstruktionen_gebaeude": + if ( + tab.name() + == "Kantonale_Ortsplanung_V1_1.Konstruktionen.Gebaeude" + ): count += 1 assert len(tab.children()) == 1 - if tab.name() == "gebaeude": + if tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude": count += 1 assert len(tab.children()) == 1 # should find 4 @@ -609,17 +612,20 @@ def _extopt_staedtische_group(self, generator, strategy): # one general and two relation editors assert len(efc.tabs()) == 3 for tab in efc.tabs(): - if tab.name() == "stadtscng_v1_1freizeit_gebaeude": + if tab.name() == "Freizeit.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "stadtscng_v1_1gewerbe_gebaeude": + if tab.name() == "Gewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 if ( - tab.name() == "kantnl_ng_v1_1konstruktionen_gebaeude" + tab.name() + == "Kantonale_Ortsplanung_V1_1.Konstruktionen.Gebaeude" ): # should not happen count += 1 - if tab.name() == "gebaeude": # should not happen + if ( + tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude" + ): # should not happen count += 1 # should find only 2 @@ -805,17 +811,20 @@ def _extopt_staedtische_hide(self, generator, strategy): # one general and two relation editors assert len(efc.tabs()) == 3 for tab in efc.tabs(): - if tab.name() == "stadtscng_v1_1freizeit_gebaeude": + if tab.name() == "Freizeit.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "stadtscng_v1_1gewerbe_gebaeude": + if tab.name() == "Gewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 if ( - tab.name() == "kantnl_ng_v1_1konstruktionen_gebaeude" + tab.name() + == "Kantonale_Ortsplanung_V1_1.Konstruktionen.Gebaeude" ): # should not happen count += 1 - if tab.name() == "gebaeude": # should not happen + if ( + tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude" + ): # should not happen count += 1 # should find only 2 assert count == 2 @@ -1250,28 +1259,28 @@ def _extopt_polymorphic_none(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 9 for tab in efc.tabs(): - if tab.name() == "gebaeude": + if tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1gewerbe_gebaeude": + if tab.name() == "Gewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1freizeit_gebaeude": + if tab.name() == "Freizeit.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1industriegewerbe_gebaeude": + if tab.name() == "IndustrieGewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1hallen_gebaeude": + if tab.name() == "Hallen.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "markthalle": + if tab.name() == "Markthalle": count += 1 assert len(tab.children()) == 1 - if tab.name() == "turnhalletyp1": + if tab.name() == "TurnhalleTyp1": count += 1 assert len(tab.children()) == 1 - if tab.name() == "turnhalletyp2": + if tab.name() == "TurnhalleTyp2": count += 1 assert len(tab.children()) == 1 # should find 8 @@ -1517,28 +1526,30 @@ def _extopt_polymorphic_group(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 8 for tab in efc.tabs(): - if tab.name() == "gebaeude": # this should not happen + if ( + tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude" + ): # this should not happen count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1gewerbe_gebaeude": + if tab.name() == "Gewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1freizeit_gebaeude": + if tab.name() == "Freizeit.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1industriegewerbe_gebaeude": + if tab.name() == "IndustrieGewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1hallen_gebaeude": + if tab.name() == "Hallen.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "markthalle": + if tab.name() == "Markthalle": count += 1 assert len(tab.children()) == 1 - if tab.name() == "turnhalletyp1": + if tab.name() == "TurnhalleTyp1": count += 1 assert len(tab.children()) == 1 - if tab.name() == "turnhalletyp2": + if tab.name() == "TurnhalleTyp2": count += 1 assert len(tab.children()) == 1 # should find 7 @@ -1764,28 +1775,30 @@ def _extopt_polymorphic_hide(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 8 for tab in efc.tabs(): - if tab.name() == "gebaeude": # this should not happen + if ( + tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude" + ): # this should not happen count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1gewerbe_gebaeude": + if tab.name() == "Gewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1freizeit_gebaeude": + if tab.name() == "Freizeit.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1industriegewerbe_gebaeude": + if tab.name() == "IndustrieGewerbe.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "polymrpng_v1_1hallen_gebaeude": + if tab.name() == "Hallen.Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "markthalle": + if tab.name() == "Markthalle": count += 1 assert len(tab.children()) == 1 - if tab.name() == "turnhalletyp1": + if tab.name() == "TurnhalleTyp1": count += 1 assert len(tab.children()) == 1 - if tab.name() == "turnhalletyp2": + if tab.name() == "TurnhalleTyp2": count += 1 assert len(tab.children()) == 1 # should find 7 @@ -2212,10 +2225,13 @@ def _extopt_baustruct_none(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 4 for tab in efc.tabs(): - if tab.name() == "kantnl_ng_v1_1konstruktionen_gebaeude": + if ( + tab.name() + == "Kantonale_Bauplanung_V1_1.Konstruktionen.Gebaeude" + ): count += 1 assert len(tab.children()) == 1 - if tab.name() == "gebaeude": + if tab.name() == "Bauplanung_V1_1.Konstruktionen.Gebaeude": count += 1 assert len(tab.children()) == 1 # should find 3 (one times gebaeude and two times kantnl_ng_v1_1konstruktionen_gebaeude because it's extended) @@ -2431,10 +2447,15 @@ def _extopt_baustruct_group(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 2 for tab in efc.tabs(): - if tab.name() == "kantnl_ng_v1_1konstruktionen_gebaeude": + if ( + tab.name() + == "Kantonale_Bauplanung_V1_1.Konstruktionen.Gebaeude" + ): count += 1 assert len(tab.children()) == 1 - if tab.name() == "gebaeude": # this should not happen + if ( + tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude" + ): # this should not happen count += 1 assert len(tab.children()) == 1 # should find 1 (one times kantnl_ng_v1_1konstruktionen_gebaeude) @@ -2613,10 +2634,12 @@ def _extopt_baustruct_hide(self, generator, strategy): # one general and four relation editors assert len(efc.tabs()) == 2 for tab in efc.tabs(): - if tab.name() == "kantnl_ng_v1_1konstruktionen_gebaeude": + if tab.name() == "Gebaeude": count += 1 assert len(tab.children()) == 1 - if tab.name() == "gebaeude": # this should not happen + if ( + tab.name() == "Ortsplanung_V1_1.Konstruktionen.Gebaeude" + ): # this should not happen count += 1 assert len(tab.children()) == 1 # should find 1 (one times kantnl_ng_v1_1konstruktionen_gebaeude) diff --git a/tests/test_translations.py b/tests/test_translations.py new file mode 100644 index 0000000..6a04745 --- /dev/null +++ b/tests/test_translations.py @@ -0,0 +1,217 @@ +""" +/*************************************************************************** + ------------------- + begin : 31.10.2024 + git sha : :%H$ + copyright : (C) 2024 by Germán Carrillo + email : german at opengis ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +import datetime +import logging +import os +import pathlib +import shutil +import tempfile + +from qgis.core import QgsProject +from qgis.testing import start_app, unittest + +from modelbaker.dataobjects.project import Project +from modelbaker.db_factory.gpkg_command_config_manager import GpkgCommandConfigManager +from modelbaker.generator.generator import Generator +from modelbaker.iliwrapper import iliimporter +from modelbaker.iliwrapper.globals import DbIliMode +from tests.utils import get_pg_connection_string, iliimporter_config + +start_app() + +test_path = pathlib.Path(__file__).parent.absolute() + + +class TestTranslations(unittest.TestCase): + @classmethod + def setUpClass(cls): + """Run before all tests.""" + cls.basetestpath = tempfile.mkdtemp() + + def test_translated_db_objects_gpkg(self): + importer = iliimporter.Importer() + importer.tool = DbIliMode.ili2gpkg + importer.configuration = iliimporter_config(importer.tool) + importer.configuration.ilimodels = "PlansDAffectation_V1_2" + importer.configuration.dbfile = os.path.join( + self.basetestpath, "tmp_translated_gpkg.gpkg" + ) + importer.configuration.inheritance = "smart2" + importer.configuration.create_basket_col = True + importer.stdout.connect(self.print_info) + importer.stderr.connect(self.print_error) + assert importer.run() == iliimporter.Importer.SUCCESS + + config_manager = GpkgCommandConfigManager(importer.configuration) + uri = config_manager.get_uri() + + generator = Generator( + DbIliMode.ili2gpkg, + uri, + importer.configuration.inheritance, + consider_basket_handling=True, + preferred_language="fr", + ) + + available_layers = generator.layers() + relations, _ = generator.relations(available_layers) + legend = generator.legend(available_layers) + + project = Project() + project.layers = available_layers + project.relations = relations + project.legend = legend + project.post_generate() + + qgis_project = QgsProject.instance() + project.create(None, qgis_project) + + count = 0 + fr_layer = None + for layer in available_layers: + if layer.name == "grundnutzung_zonenflaeche": + assert layer.alias == "AffectationPrimaire_SurfaceDeZones" + fr_layer = layer.layer + count += 1 + fields = fr_layer.fields() + field_idx = fields.lookupField("publiziertab") + assert field_idx != -1 + field = fields.field(field_idx) + assert field.name() == "publiziertab" + assert field.alias() == "publieDepuis" + + edit_form_config = fr_layer.editFormConfig() + tabs = edit_form_config.tabs() + tab_list = [tab.name() for tab in tabs] + expected_tab_list = [ + "General", + "Document", + "ContenuPonctuel", + "ZoneSuperposee", + "ContenuLineaire", + ] + assert len(tab_list) == len(expected_tab_list) + assert set(tab_list) == set(expected_tab_list) + + # check if the layers have been considered + assert count == 1 + assert fr_layer + + # Check translated relation + rels = qgis_project.relationManager().referencedRelations(fr_layer) + assert len(rels) == 1 + assert ( + rels[0].id() + == "geometrie_dokument_geometrie_grundnutzung_zonenflaeche_grundnutzung_zonenflaeche_T_Id" + ) + assert ( + rels[0].name() + == "Geometrie_Document_(Geometrie)_AffectationPrimaire_SurfaceDeZones_(T_Id)" + ) + + def test_translated_db_objects_pg(self): + importer = iliimporter.Importer() + importer.tool = DbIliMode.ili2pg + importer.configuration = iliimporter_config(importer.tool) + importer.configuration.ilimodels = "PlansDAffectation_V1_2" + importer.configuration.dbschema = "tid_{:%Y%m%d%H%M%S%f}".format( + datetime.datetime.now() + ) + importer.configuration.inheritance = "smart2" + importer.configuration.create_basket_col = True + importer.stdout.connect(self.print_info) + importer.stderr.connect(self.print_error) + assert importer.run() == iliimporter.Importer.SUCCESS + + generator = Generator( + DbIliMode.ili2pg, + get_pg_connection_string(), + importer.configuration.inheritance, + importer.configuration.dbschema, + consider_basket_handling=True, + preferred_language="fr", + ) + + available_layers = generator.layers() + relations, _ = generator.relations(available_layers) + legend = generator.legend(available_layers) + + project = Project() + project.layers = available_layers + project.relations = relations + project.legend = legend + project.post_generate() + + qgis_project = QgsProject.instance() + project.create(None, qgis_project) + + count = 0 + fr_layer = None + for layer in available_layers: + if layer.name == "grundnutzung_zonenflaeche": + assert layer.alias == "AffectationPrimaire_SurfaceDeZones" + fr_layer = layer.layer + count += 1 + fields = fr_layer.fields() + field_idx = fields.lookupField("publiziertab") + assert field_idx != -1 + field = fields.field(field_idx) + assert field.name() == "publiziertab" + assert field.alias() == "publieDepuis" + + edit_form_config = fr_layer.editFormConfig() + tabs = edit_form_config.tabs() + tab_list = [tab.name() for tab in tabs] + expected_tab_list = [ + "General", + "Document", + "ContenuPonctuel", + "ZoneSuperposee", + "ContenuLineaire", + ] + assert len(tab_list) == len(expected_tab_list) + assert set(tab_list) == set(expected_tab_list) + + # check if the layers have been considered + assert count == 1 + assert fr_layer + + # Check translated relation + rels = qgis_project.relationManager().referencedRelations(fr_layer) + assert len(rels) == 1 + assert rels[0].id() == "geometrie_dokument_geometr_grndntzng_znnflche_fkey" + assert ( + rels[0].name() + == "Geometrie_Document_(Geometrie)_AffectationPrimaire_SurfaceDeZones_(t_id)" + ) + + def print_info(self, text): + logging.info(text) + + def print_error(self, text): + logging.error(text) + + def tearDown(self): + QgsProject.instance().removeAllMapLayers() + + @classmethod + def tearDownClass(cls): + """Run after all tests.""" + shutil.rmtree(cls.basetestpath, True) diff --git a/tests/testdata/ilimodels/Nutzungsplanung_V1_2.ili b/tests/testdata/ilimodels/Nutzungsplanung_V1_2.ili new file mode 100644 index 0000000..4a64e52 --- /dev/null +++ b/tests/testdata/ilimodels/Nutzungsplanung_V1_2.ili @@ -0,0 +1,226 @@ +INTERLIS 2.3; + +/** Minimales Geodatenmodell + * Nutzungsplanung (kantonal/kommunal) + * Geobasisdatensatz Nr. 73 + * TRANSLATION OF-Modelle: PlansDAffectation_V1_2.ili, PianiDiUtilizzazione_V1_2.ili + */ + +!! Version | Who | Modification +!!------------------------------------------------------------------------------ +!! 2023-03-20 | ARE | - DOMAIN Gebietseinteilung fällt weg +!! | - CLASS Grundnutzung_Zonenflaeche: Geometrie vom Typ Einzelflaeche (SURFACE) +!! | - CLASS Grundnutzung_Zonenflaeche: CONSTRAINT zur Gewährleistung der AREA-Topologie +!! | - CLASS Typ und Typ_Kt: Attribut Code, Feldlänge 40 Zeichen +!!------------------------------------------------------------------------------ +!! 2021-11-19 | KOGIS | Localisation_V1 replaced by LocalisationCH_V1 +!!------------------------------------------------------------------------------ +!! 2021-09-01 | ARE | Version 1.2 +!! | Anpassungen an das ÖREB-Rahmenmodell Version 2.0 vom 14.04.2021 +!! | - DOMAIN RechtsStatus angepasst, DokumentTyp neu +!! | - STRUCTURE LocalisedBlob und MultilingualBlob neu +!! | - CLASS Geometrie: neues Attribut publiziertBis +!! | - CLASS Dokument angepasst an ÖREB-Rahmenmodell +!! | - CLASS Amt angepasst an ÖREB-Rahmenmodell +!! | Weitere technische Anpassungen +!! | - MODEL Nutzungsplanung_V1_2: nur noch ein Modell, separate Modelle für LV03 und Katalog nicht mehr notwendig +!! | - CLASS Hauptnutzung_CH heisst neu Catalogue_CH und ist mehrsprachig (Text als MultilingualText) +!! | - ASSOCIATION Geometrie_Dokument zusätzlich eingefügt, um eine direkte Verknüpfung von Geometrie und Dokument zu ermöglichen +!! | - TOPIC Geobasisdaten neu mit BASKET OID vom Typ TypeID +!! | - CLASS Datenbestand: Attribut BasketID neu vom Typ TypeID, neues Meta-Attribut zur Überprüfung der ID +!! +!!------------------------------------------------------------------------------ + +!!@ technicalContact=mailto:info@are.admin.ch +!!@ furtherInformation=https://www.are.admin.ch/mgm +!!@ IDGeoIV=73 +MODEL Nutzungsplanung_V1_2 (de) +AT "https://models.geo.admin.ch/ARE/" +VERSION "2023-03-20" = + IMPORTS CHAdminCodes_V1,InternationalCodes_V1,LocalisationCH_V1,GeometryCHLV95_V1; + + DOMAIN + + Einzelflaeche = SURFACE WITH (ARCS,STRAIGHTS) VERTEX GeometryCHLV95_V1.Coord2 WITHOUT OVERLAPS>0.05; + + TypeID = OID TEXT*60; + + RechtsStatus = ( + inKraft, + AenderungMitVorwirkung, + AenderungOhneVorwirkung + ); + + DokumentTyp = ( + Rechtsvorschrift, + GesetzlicheGrundlage, + Hinweis + ); + + Verbindlichkeit = ( + Nutzungsplanfestlegung, + orientierend, + hinweisend, + wegleitend + ); + + STRUCTURE LocalisedUri = + Language : InternationalCodes_V1.LanguageCode_ISO639_1; + Text : MANDATORY URI; + END LocalisedUri; + + STRUCTURE MultilingualUri = + LocalisedText : BAG {1..*} OF Nutzungsplanung_V1_2.LocalisedUri; + UNIQUE (LOCAL) LocalisedText: Language; + END MultilingualUri; + + STRUCTURE LocalisedBlob = + Language : InternationalCodes_V1.LanguageCode_ISO639_1; + Blob : MANDATORY BLACKBOX BINARY; + END LocalisedBlob; + + STRUCTURE MultilingualBlob = + LocalisedBlob : BAG {1..*} OF Nutzungsplanung_V1_2.LocalisedBlob; + UNIQUE (LOCAL) LocalisedBlob: Language; + END MultilingualBlob; + + TOPIC Catalogue_CH = + BASKET OID AS TypeID; + + CLASS Catalogue_CH (FINAL) = + OID AS TypeID; + Code : MANDATORY 11 .. 99; + Designation : MANDATORY LocalisationCH_V1.MultilingualText; + END Catalogue_CH; + + END Catalogue_CH; + + TOPIC Rechtsvorschriften = + DEPENDS ON Nutzungsplanung_V1_2.Catalogue_CH; + + CLASS Dokument = + Typ : MANDATORY Nutzungsplanung_V1_2.DokumentTyp; + Titel : MANDATORY LocalisationCH_V1.MultilingualText; + Abkuerzung : LocalisationCH_V1.MultilingualText; + OffizielleNr : LocalisationCH_V1.MultilingualText; + NurInGemeinde : CHAdminCodes_V1.CHMunicipalityCode; + TextImWeb : Nutzungsplanung_V1_2.MultilingualUri; + Dokument : Nutzungsplanung_V1_2.MultilingualBlob; + AuszugIndex : MANDATORY -1000 .. 1000; + Rechtsstatus : MANDATORY Nutzungsplanung_V1_2.RechtsStatus; + publiziertAb : MANDATORY INTERLIS.XMLDate; + publiziertBis : INTERLIS.XMLDate; + END Dokument; + + END Rechtsvorschriften; + + TOPIC Geobasisdaten = + BASKET OID AS TypeID; + DEPENDS ON Nutzungsplanung_V1_2.Catalogue_CH,Nutzungsplanung_V1_2.Rechtsvorschriften; + + CLASS Geometrie (ABSTRACT) = + publiziertAb : MANDATORY INTERLIS.XMLDate; + publiziertBis : INTERLIS.XMLDate; + Rechtsstatus : MANDATORY Nutzungsplanung_V1_2.RechtsStatus; + Bemerkungen : MTEXT; + END Geometrie; + + CLASS Typ = + Code : MANDATORY TEXT*40; + Bezeichnung : MANDATORY TEXT*80; + Abkuerzung : TEXT*12; + Verbindlichkeit : MANDATORY Nutzungsplanung_V1_2.Verbindlichkeit; + Nutzungsziffer : 0.00 .. 9.00; + Nutzungsziffer_Art : TEXT*40; + Bemerkungen : MTEXT; + Symbol : BLACKBOX BINARY; + END Typ; + + CLASS Typ_Kt = + Code : MANDATORY TEXT*40; + Bezeichnung : MANDATORY TEXT*80; + Abkuerzung : TEXT*12; + Bemerkungen : MTEXT; + END Typ_Kt; + + CLASS Grundnutzung_Zonenflaeche + EXTENDS Geometrie = + Geometrie : MANDATORY Nutzungsplanung_V1_2.Einzelflaeche; + SET CONSTRAINT WHERE Rechtsstatus == #inKraft: + INTERLIS.areAreas(ALL, UNDEFINED, >> Geometrie); + END Grundnutzung_Zonenflaeche; + + CLASS Linienbezogene_Festlegung + EXTENDS Geometrie = + Geometrie : MANDATORY GeometryCHLV95_V1.Line; + END Linienbezogene_Festlegung; + + CLASS Objektbezogene_Festlegung + EXTENDS Geometrie = + Geometrie : MANDATORY GeometryCHLV95_V1.Coord2; + END Objektbezogene_Festlegung; + + CLASS Ueberlagernde_Festlegung + EXTENDS Geometrie = + Geometrie : MANDATORY Nutzungsplanung_V1_2.Einzelflaeche; + END Ueberlagernde_Festlegung; + + ASSOCIATION Typ_Dokument = + Typ (EXTERNAL) -- {0..*} Typ; + Dokument (EXTERNAL) -- {0..*} Nutzungsplanung_V1_2.Rechtsvorschriften.Dokument; + END Typ_Dokument; + + ASSOCIATION Geometrie_Dokument = + Geometrie (EXTERNAL) -- {0..*} Geometrie; + Dokument (EXTERNAL) -- {0..*} Nutzungsplanung_V1_2.Rechtsvorschriften.Dokument; + END Geometrie_Dokument; + + ASSOCIATION Typ_Geometrie = + Geometrie -- {0..*} Geometrie; + Typ -<> {1} Typ; + END Typ_Geometrie; + + ASSOCIATION Typ_Typ_Kt = + Typ -- {0..*} Typ; + Typ_Kt (EXTERNAL) -<> {1} Typ_Kt; + END Typ_Typ_Kt; + + ASSOCIATION TypKt_CatalogueCH = + Typ_Kt -- {0..*} Typ_Kt; + Catalogue_CH (EXTERNAL) -- {1} Nutzungsplanung_V1_2.Catalogue_CH.Catalogue_CH; + END TypKt_CatalogueCH; + + END Geobasisdaten; + + TOPIC TransferMetadaten = + DEPENDS ON Nutzungsplanung_V1_2.Rechtsvorschriften; + + CLASS Amt = + Name : MANDATORY LocalisationCH_V1.MultilingualText; + AmtImWeb : Nutzungsplanung_V1_2.MultilingualUri; + UID : TEXT*12; + Zeile1 : TEXT*80; + Zeile2 : TEXT*80; + Strasse : TEXT*100; + Hausnr : TEXT*7; + PLZ : TEXT*4; + Ort : TEXT*40; + UNIQUE UID; + END Amt; + + CLASS Datenbestand = + !!@ basketRef=Nutzungsplanung_V1_2.Geobasisdaten + BasketID : MANDATORY TypeID; + Stand : MANDATORY INTERLIS.XMLDate; + Lieferdatum : INTERLIS.XMLDate; + Bemerkungen : MTEXT; + END Datenbestand; + + ASSOCIATION zustStelle_Daten = + zustaendigeStelle -<> {1} Amt; + Datenbestand -- {0..*} Datenbestand; + END zustStelle_Daten; + + END TransferMetadaten; + +END Nutzungsplanung_V1_2. diff --git a/tests/testdata/ilimodels/PlansDAffectation_V1_2.ili b/tests/testdata/ilimodels/PlansDAffectation_V1_2.ili new file mode 100644 index 0000000..64f41ea --- /dev/null +++ b/tests/testdata/ilimodels/PlansDAffectation_V1_2.ili @@ -0,0 +1,225 @@ +INTERLIS 2.3; + +/** Modèle de géodonnées minimal + * Plans d'affectation (cantonaux/communaux) + * Jeu de géodonnées de base No. 73 + */ + +!! Version | Who | Modification +!!------------------------------------------------------------------------------ +!! 2023-03-20 | ARE | - DOMAIN SurfacePartition supprimé +!! | - CLASS AffectationPrimaire_SurfaceDeZones: Geometrie du type SurfaceUnique (SURFACE) +!! | - CLASS AffectationPrimaire_SurfaceDeZones: CONSTRAINT pour garantir la topologie AREA +!! | - CLASS Type et Type_Ct: Attribut Code, Longeur de champ 40 caractères +!!------------------------------------------------------------------------------ +!! 2021-11-19 | KOGIS | Localisation_V1 replaced by LocalisationCH_V1 +!!------------------------------------------------------------------------------ +!! 2021-09-01 | ARE | Version 1.2 +!! | Adaption au modèle-cadre RDPPF version 2.0 du 14 avril 2021 : +!! | - DOMAIN StatutJuridique adapté, TypeDocument nouveau +!! | - STRUCTURE LocalisedBlob et MultilingualBlob nouveaux +!! | - CLASS Geometrie, Zone_SensibiliteAuBruit, LimiteDeLaForet_Ligne, DistancesParRapportALa Foret_Ligne: nouvel attribut publieJusque +!! | - CLASS Document adapté au modèle-cadre RDPPF +!! | - CLASS Service adapté au modèle-cadre RDPPF +!! | Modifications techniques ultérieures +!! | - MODEL PlansDAffectation_V1_2 : qu’un modèle, les deux modèles separés pour MN03 et le catalogue ne sont plus nécessaires +!! | - CLASS AffectationPrincipale_CH s’appelle maintenant Catalogue_CH, elle est multilingue (Designation comme MultilingualText). +!! | - ASSOCIATION Geometrie_Document inséré additionellement pour permettre un lien direct entre les classes Geometrie et Document +!! | - TOPIC GeodonneesDeBase maintenant avec BASKET OID du type TypeID +!! | - CLASS JeuDeDonnees : Attribut BasketID maintenant du type TypeID, nouvel méta-attribut pour la vérification de l’ID +!!------------------------------------------------------------------------------ + +!!@ technicalContact=mailto:info@are.admin.ch +!!@ furtherInformation=https://www.are.admin.ch/mgm +!!@ IDGeoIV=73 +MODEL PlansDAffectation_V1_2 (fr) +AT "https://models.geo.admin.ch/ARE/" +VERSION "2023-03-20" +TRANSLATION OF Nutzungsplanung_V1_2 ["2023-03-20"] = + IMPORTS CHAdminCodes_V1,InternationalCodes_V1,LocalisationCH_V1,GeometryCHLV95_V1; + + DOMAIN + + SurfaceUnique = SURFACE WITH (ARCS,STRAIGHTS) VERTEX GeometryCHLV95_V1.Coord2 WITHOUT OVERLAPS>0.05; + + TypeID = OID TEXT*60; + + StatutJuridique = ( + enVigueur, + ModificationAvecEffetAnticipe, + ModificationSansEffetAnticipe + ); + + TypeDocument = ( + DispositionJuridique, + BaseLegale, + Renvoi + ); + + ForceObligatoire = ( + Contenu_contraignant, + Contenu_informatif, + Contenu_indicatif, + Contenu_dAideALaMiseEnOeuvre + ); + + STRUCTURE LocalisedUri = + Language : InternationalCodes_V1.LanguageCode_ISO639_1; + Text : MANDATORY URI; + END LocalisedUri; + + STRUCTURE MultilingualUri = + LocalisedText : BAG {1..*} OF PlansDAffectation_V1_2.LocalisedUri; + UNIQUE (LOCAL) LocalisedText: Language; + END MultilingualUri; + + STRUCTURE LocalisedBlob = + Language : InternationalCodes_V1.LanguageCode_ISO639_1; + Blob : MANDATORY BLACKBOX BINARY; + END LocalisedBlob; + + STRUCTURE MultilingualBlob = + LocalisedBlob : BAG {1..*} OF PlansDAffectation_V1_2.LocalisedBlob; + UNIQUE (LOCAL) LocalisedBlob: Language; + END MultilingualBlob; + + TOPIC Catalogue_CH = + BASKET OID AS TypeID; + + CLASS Catalogue_CH (FINAL) = + OID AS TypeID; + Code : MANDATORY 11 .. 99; + Designation : MANDATORY LocalisationCH_V1.MultilingualText; + END Catalogue_CH; + + END Catalogue_CH; + + TOPIC DispositionsJuridiques = + DEPENDS ON PlansDAffectation_V1_2.Catalogue_CH; + + CLASS Document = + Type : MANDATORY PlansDAffectation_V1_2.TypeDocument; + Titre : MANDATORY LocalisationCH_V1.MultilingualText; + Abreviation : LocalisationCH_V1.MultilingualText; + NoOfficiel : LocalisationCH_V1.MultilingualText; + SeulementCommune : CHAdminCodes_V1.CHMunicipalityCode; + TexteSurInternet : PlansDAffectation_V1_2.MultilingualUri; + Document : PlansDAffectation_V1_2.MultilingualBlob; + IndexExtrait : MANDATORY -1000 .. 1000; + StatutJuridique : MANDATORY PlansDAffectation_V1_2.StatutJuridique; + publieDepuis : MANDATORY INTERLIS.XMLDate; + publieJusque : INTERLIS.XMLDate; + END Document; + + END DispositionsJuridiques; + + TOPIC GeodonneesDeBase = + BASKET OID AS TypeID; + DEPENDS ON PlansDAffectation_V1_2.Catalogue_CH,PlansDAffectation_V1_2.DispositionsJuridiques; + + CLASS Geometrie (ABSTRACT) = + publieDepuis : MANDATORY INTERLIS.XMLDate; + publieJusque : INTERLIS.XMLDate; + StatutJuridique : MANDATORY PlansDAffectation_V1_2.StatutJuridique; + Remarques : MTEXT; + END Geometrie; + + CLASS Type = + Code : MANDATORY TEXT*40; + Designation : MANDATORY TEXT*80; + Abreviation : TEXT*12; + ForceObligatoire : MANDATORY PlansDAffectation_V1_2.ForceObligatoire; + IndiceUtilisation : 0.00 .. 9.00; + IndiceUtilisationType : TEXT*40; + Remarques : MTEXT; + Symbole : BLACKBOX BINARY; + END Type; + + CLASS Type_Ct = + Code : MANDATORY TEXT*40; + Designation : MANDATORY TEXT*80; + Abreviation : TEXT*12; + Remarques : MTEXT; + END Type_Ct; + + CLASS AffectationPrimaire_SurfaceDeZones + EXTENDS Geometrie = + Geometrie : MANDATORY PlansDAffectation_V1_2.SurfaceUnique; + SET CONSTRAINT WHERE StatutJuridique == #enVigueur: + INTERLIS.areAreas(ALL, UNDEFINED, >> Geometrie); + END AffectationPrimaire_SurfaceDeZones; + + CLASS ContenuLineaire + EXTENDS Geometrie = + Geometrie : MANDATORY GeometryCHLV95_V1.Line; + END ContenuLineaire; + + CLASS ContenuPonctuel + EXTENDS Geometrie = + Geometrie : MANDATORY GeometryCHLV95_V1.Coord2; + END ContenuPonctuel; + + CLASS ZoneSuperposee + EXTENDS Geometrie = + Geometrie : MANDATORY PlansDAffectation_V1_2.SurfaceUnique; + END ZoneSuperposee; + + ASSOCIATION Type_Document = + Type (EXTERNAL) -- {0..*} Type; + Document (EXTERNAL) -- {0..*} PlansDAffectation_V1_2.DispositionsJuridiques.Document; + END Type_Document; + + ASSOCIATION Geometrie_Document = + Geometrie (EXTERNAL) -- {0..*} Geometrie; + Document (EXTERNAL) -- {0..*} PlansDAffectation_V1_2.DispositionsJuridiques.Document; + END Geometrie_Document; + + ASSOCIATION Type_Geometrie = + Geometrie -- {0..*} Geometrie; + Type -<> {1} Type; + END Type_Geometrie; + + ASSOCIATION Type_Type_Ct = + Type -- {0..*} Type; + Type_Ct (EXTERNAL) -<> {1} Type_Ct; + END Type_Type_Ct; + + ASSOCIATION TypeCt_CatalogueCH = + Type_Ct -- {0..*} Type_Ct; + Catalogue_CH (EXTERNAL) -- {1} PlansDAffectation_V1_2.Catalogue_CH.Catalogue_CH; + END TypeCt_CatalogueCH; + + END GeodonneesDeBase; + + TOPIC MetadonneesTransfert = + DEPENDS ON PlansDAffectation_V1_2.DispositionsJuridiques; + + CLASS Service = + Nom : MANDATORY LocalisationCH_V1.MultilingualText; + ServiceSurInternet : PlansDAffectation_V1_2.MultilingualUri; + IDE : TEXT*12; + Ligne1 : TEXT*80; + Ligne2 : TEXT*80; + Rue : TEXT*100; + Numero : TEXT*7; + NPA : TEXT*4; + Localite : TEXT*40; + UNIQUE IDE; + END Service; + + CLASS JeuDeDonnees = + !!@ basketRef=PlansDAffectation_V1_2.GeodonneesDeBase + BasketID : MANDATORY TypeID; + Version : MANDATORY INTERLIS.XMLDate; + DateDeLivraison : INTERLIS.XMLDate; + Remarques : MTEXT; + END JeuDeDonnees; + + ASSOCIATION Donnees_orgResp = + OrganismeResponsable -<> {1} Service; + JeuDeDonnees -- {0..*} JeuDeDonnees; + END Donnees_orgResp; + + END MetadonneesTransfert; + +END PlansDAffectation_V1_2.