Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion core/admin/_admin_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def project_type_for_schema(
WHERE c.relname = 'sys_version'
AND c.relkind IN ('r', 'p')
AND n.nspname = ANY(%s::text[])
AND a.attname = ANY(ARRAY['giswater', 'version', 'addparam', 'date'])
AND a.attname = ANY(ARRAY['giswater', 'version', 'addparam', 'date', 'language', 'project_type'])
AND a.attnum > 0
AND NOT a.attisdropped
"""
Expand Down Expand Up @@ -694,3 +694,87 @@ def find_inventory_row(
if want_kind and str(row.get("kind") or "").upper() == want_kind:
return row
return None


_TRANSLATABLE_KINDS = frozenset({"ws", "ud", "am", "cm"})


def fetch_schema_translation_info(
fetcher: RowFetcher = _tools_db_fetch,
*,
kinds: frozenset[str] | set[str] | None = None,
) -> list[dict[str, Any]]:
"""
Return [{schema, kind, version, language}, ...] for Giswater schemas that
expose ``sys_version``. Missing/legacy columns are tolerated (empty strings).
"""
wanted = {str(k).lower() for k in (kinds or _TRANSLATABLE_KINDS)}
names = fetch_schema_names_with_sys_version(fetcher)
if not names:
return []

columns_by_schema = _sys_version_columns_by_schema(names, fetcher)
result: list[dict[str, Any]] = []

for schema_name in sorted(names):
columns = columns_by_schema.get(schema_name, set())
version_col = _version_column_for_schema(schema_name, columns)
if not version_col:
continue

select_cols = ["project_type", version_col]
has_language = "language" in columns
if has_language:
select_cols.append("language")

row = fetcher(
f"SELECT {', '.join(select_cols)} "
f"FROM {_quote_ident(schema_name)}.sys_version "
"ORDER BY id DESC LIMIT 1",
None,
)
if not row or not row[0]:
continue

values = row[0]
kind = str(values[0] or "").strip().lower()
if not kind or kind not in wanted:
continue

version = str(values[1] or "") if len(values) > 1 else ""
language = ""
if has_language and len(values) > 2:
language = str(values[2] or "")

result.append({
"schema": schema_name,
"kind": kind,
"version": version,
"language": language,
})
return result


def fetch_multilang_operative_languages(
fetcher: RowFetcher = _tools_db_fetch,
) -> set[str]:
"""
Return operative multilang language ids (folder form) when the multilang
schema exists. Missing schema/tables return an empty set (non-fatal).
"""
try:
if not schema_exists("multilang", fetcher):
return set()
except Exception:
return set()

try:
from .i18n_baseline_seed import (
fetch_seeded_language_ids,
normalize_language_folder,
)
ids = fetch_seeded_language_ids(fetcher)
except Exception:
return set()

return {normalize_language_folder(lang) for lang in ids if lang}
177 changes: 148 additions & 29 deletions core/admin/admin_btn.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from ...libs import lib_vars, tools_qt, tools_qgis, tools_log, tools_db, tools_os
from ..ui.docker import GwDocker
from ..threads.schema_builder_task import GwSchemaBuilderTask, load_kind_manifest
from ..threads.multilang_schema_task import GwMultilangSchemaTask
from ..threads.project_schema_copy import GwCopySchemaTask
from ..threads.project_schema_rename import GwRenameSchemaTask
from ..threads.project_schema_vacuum import GwVacuumSchemaTask
Expand Down Expand Up @@ -1596,8 +1597,20 @@ def reload_connection_for_manage_schemas(self, connection_name: str) -> bool:
update_info = getattr(self, "_manage_schemas_update_system_info", None)
if update_info:
update_info()
self._ensure_language_packages_for_connection(force=True)
return True

def _ensure_language_packages_for_connection(self, *, force: bool = False) -> None:
"""Start automatic language-file provisioning for the active DB connection."""
try:
from .i18n_provision import ensure_language_packages_after_connection
ensure_language_packages_after_connection(force=force)
except Exception as exc:
msg = "Automatic language provisioning failed to start: {0}"
msg_params = (exc,)
tools_log.log_warning(msg, msg_params=msg_params)
self._i18n_provision_after_load = False

def _start_admin_load_sync(self, connection_name):
self._extensions_checked = False
self._cached_pg_versions = None
Expand Down Expand Up @@ -1703,6 +1716,10 @@ def _apply_admin_load_result(self, result: AdminLoadResult, connection_name):
if manage_cmb:
manage_cmb(connection_name)

force = bool(getattr(self, "_i18n_provision_after_load", False))
self._i18n_provision_after_load = False
self._ensure_language_packages_for_connection(force=force)

def _finalize_admin_permissions_and_status(self):
message = ''
if not tools_db.check_role(self.username, is_admin=True) and not getattr(self, '_admin_show_dialog', False):
Expand Down Expand Up @@ -2489,6 +2506,8 @@ def _event_change_connection(self):
self.username = self._get_user_connection(connection_name)
tools_db.current_user = None
self._schema_cache.pop(connection_name, None)
# Re-check language packages after the new connection finishes loading.
self._i18n_provision_after_load = True
self._start_admin_load(connection_name, try_set_connection=True, show_dialog=True)

def _set_last_connection(self, connection_name):
Expand Down Expand Up @@ -2813,22 +2832,30 @@ def _set_signals_create_project(self):

def _open_manage_schemas(self):
from .manage_schemas_dlg import GwManageSchemasDialog

dlg = getattr(self, '_manage_schemas_dlg', None)
if dlg is not None and not isdeleted(dlg) and dlg.isVisible():
tools_gw.focus_open_dialog(dlg)
return

dlg = GwManageSchemasDialog(self, parent=self.dlg_readsql)
tools_gw.load_settings(dlg)
dlg.apply_fixed_geometry()
dlg.apply_scroll_geometry()
self._manage_schemas_refresh = dlg._refresh_inventory
self._manage_schemas_update_system_info = dlg._update_system_info
self._manage_schemas_sync_connection = dlg._sync_connection_combo
self._manage_schemas_dlg = dlg
lib_vars.session_vars["message_parent"] = dlg
try:
dlg.exec()
finally:
dlg.finished.connect(self._on_manage_schemas_closed)
tools_gw.open_dialog(dlg, dlg_name='admin_manage_schemas')

def _on_manage_schemas_closed(self, *_args):
if lib_vars.session_vars.get("message_parent") is getattr(self, '_manage_schemas_dlg', None):
lib_vars.session_vars["message_parent"] = None
self._manage_schemas_refresh = None
self._manage_schemas_update_system_info = None
self._manage_schemas_sync_connection = None
self._manage_schemas_dlg = None
self._manage_schemas_refresh = None
self._manage_schemas_update_system_info = None
self._manage_schemas_sync_connection = None
self._manage_schemas_dlg = None

def _open_create_project(self):
""""""
Expand Down Expand Up @@ -3119,9 +3146,75 @@ def _delete_other_schema(self, schema):
tools_qt.show_info_box(msg, "Info")
self._refresh_admin_catalog_cache()
self._set_buttons_enabled()
refresh = getattr(self, "_manage_schemas_refresh", None)
if refresh:
refresh()
else:
tools_qt.show_info_box(f"Delete schema failed: {fx.error}", "Error")

def _multilang_stored_seeded_schemas(self) -> set[str]:
"""Project types recorded at last multilang seed (prefer addparam)."""
from .i18n_baseline_seed import (
fetch_seeded_project_types_from_multilang,
parse_stored_seeded_project_types,
)

if not admin_catalog.schema_exists("multilang"):
return set()

row = tools_db.get_row(
"SELECT addparam FROM multilang.sys_version ORDER BY id DESC LIMIT 1"
)
if row and row[0]:
try:
addparam = json.loads(row[0]) if isinstance(row[0], str) else row[0]
except (TypeError, ValueError):
addparam = None
if isinstance(addparam, dict) and (
"seeded_project_types" in addparam or "seeded_schemas" in addparam
):
return parse_stored_seeded_project_types(addparam)

# Legacy multilang schemas without seeded_project_types in addparam.
return fetch_seeded_project_types_from_multilang(
fetcher=admin_catalog._tools_db_fetch,
)

def _multilang_current_seed_targets(self, inventory_rows=None) -> set[str]:
"""Project types that should be present in multilang seed (ws/ud/am/cm)."""
from .i18n_baseline_seed import translatable_project_types_with_baseline

return set(translatable_project_types_with_baseline(self.sql_dir))

def _multilang_schemas_out_of_sync(self, inventory_rows=None) -> bool:
"""True when project types differ from the last multilang seed."""
from .i18n_baseline_seed import seeded_project_types_out_of_sync

if not admin_catalog.schema_exists("multilang"):
return False
current = self._multilang_current_seed_targets(inventory_rows)
stored = self._multilang_stored_seeded_schemas()
return seeded_project_types_out_of_sync(current, stored)

def _multilang_baseline_changed(self) -> bool:
"""True when bundled en_US baseline SQL differs from the last seed."""
from .i18n_baseline_seed import baseline_needs_reseed

if not admin_catalog.schema_exists("multilang"):
return False
row = tools_db.get_row(
"SELECT addparam FROM multilang.sys_version ORDER BY id DESC LIMIT 1"
)
stored = None
if row and row[0]:
try:
addparam = json.loads(row[0]) if isinstance(row[0], str) else row[0]
if isinstance(addparam, dict):
stored = addparam.get("seed_baseline_fingerprint")
except (TypeError, ValueError):
pass
return baseline_needs_reseed(self.sql_dir, stored)

def _build_replace_dlg(self, replace_json):

# Build the dialog
Expand Down Expand Up @@ -4194,49 +4287,84 @@ def _update_utils(self, schema_name=None, on_done=None):
on_done=on_done,
)

def _create_i18n(self):
"""Create the (singleton) i18n satellite schema via the engine."""
def _create_i18n(self, manage_schemas_dlg=None):
"""Create multilang schema and seed en_US baseline translations."""
if admin_catalog.schema_exists('multilang'):
tools_qgis.show_message("Schema multilang already exists.", Qgis.MessageLevel.Info)
return
return False
from .i18n_baseline_seed import invalidate_baseline_fingerprint_cache
invalidate_baseline_fingerprint_cache(self.sql_dir)
bp = BuildParams(
schema_name='multilang',
srid=str(self.project_epsg or "25831"),
locale=self.locale,
locale="en_US",
plugin_version=str(self.plugin_version),
profile='empty',
register_is_new='true',
sql_root=self.sql_dir,
)
self._submit_builder(
'multilang',
self._submit_multilang_task(
bp,
description='Create multilang schema',
on_done=partial(self._on_builder_done_other_update, 'multilang'),
on_done=partial[None](self._on_builder_done_other_update, 'multilang'),
manage_schemas_dlg=manage_schemas_dlg,
)
return True

def _submit_multilang_task(
self,
params,
*,
description=None,
on_done=None,
manage_schemas_dlg=None,
):
"""Queue multilang build + baseline seed on the QGIS task manager."""
self._open_schema_build_message_log()
self.schema_build_progress_hint = ""
self.error_count = 0
self.t0 = time()
self.timer = QTimer()
self.timer.start(1000)

desc = description or "Create multilang schema"
task = GwMultilangSchemaTask(
self,
params,
description=desc,
timer=self.timer,
on_done=on_done,
manage_schemas_dlg=manage_schemas_dlg,
)
self.task_create_schema = task
QgsApplication.taskManager().addTask(task)
QgsApplication.taskManager().triggerTask(task)
return task

def _update_i18n(self, on_done=None):
"""Run the i18n 'update' profile in place."""
def _update_i18n(self, on_done=None, manage_schemas_dlg=None):
"""Run multilang schema update and re-seed baseline translations."""
from .i18n_baseline_seed import invalidate_baseline_fingerprint_cache
invalidate_baseline_fingerprint_cache(self.sql_dir)
row = tools_db.get_row(
"SELECT giswater FROM multilang.sys_version ORDER BY id DESC LIMIT 1"
)
current_version = row[0] if row and row[0] else "0.0.0"
bp = BuildParams(
schema_name='multilang',
srid=str(self.project_epsg or "25831"),
locale=self.locale,
locale="en_US",
plugin_version=str(self.plugin_version),
project_version=str(current_version),
profile='update',
run_mode='upgrade',
sql_root=self.sql_dir,
)
callback = on_done if on_done is not None else partial(self._on_builder_done_other_update, 'multilang')
self._submit_builder(
'multilang',
self._submit_multilang_task(
bp,
description="Update multilang schema",
on_done=callback,
manage_schemas_dlg=manage_schemas_dlg,
)

def _update_cibs(self, on_done=None):
Expand Down Expand Up @@ -4356,15 +4484,6 @@ def _update_cm(self, parent_schema=None, parent_type=None, cm_schema=None, on_do
def _on_builder_done_other_update(self, schema_name, result):
if not result.ok:
self.error_count += 1
elif schema_name == 'multilang' and self.schema_name:
lang_value = json.dumps({"lang": self.locale or "en_US"}).replace("'", "''")
sql = (f"UPDATE {self.schema_name}.config_param_system "
f"SET value = '{lang_value}', isenabled = true "
f"WHERE parameter = 'utils_language_ui'")
tools_log.log_info("Task '{0}' execute sql: '{1}'",
msg_params=("Update multilang language", sql,))
if not tools_db.execute_sql(sql):
self.error_count += 1
self.manage_other_process_result()

def _calculate_elapsed_time(self, dialog):
Expand Down
Loading