From 39c4ac416ac5a9a60a6f77445df62129d02dfe87 Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Tue, 31 Oct 2023 13:03:20 +0100 Subject: [PATCH] Rewrite all project rules checker, add a new table in settings for results and rules --- lizmap/definitions/lizmap_cloud.py | 10 + lizmap/definitions/online_help.py | 12 +- lizmap/definitions/qgis_settings.py | 4 +- lizmap/definitions/warnings.py | 15 - lizmap/dialogs/main.py | 65 ++- lizmap/models/__init__.py | 3 - lizmap/models/check_project.py | 222 --------- lizmap/plugin.py | 414 ++++++---------- lizmap/project_checker_tools.py | 141 ++---- lizmap/resources/css/log.css | 6 + lizmap/resources/ui/ui_lizmap.ui | 97 +++- lizmap/saas.py | 18 +- lizmap/test/manuel_table_checks.py | 16 - lizmap/test/test_table_checks.py | 35 +- lizmap/test/test_ui.py | 2 +- lizmap/widgets/check_project.py | 721 +++++++++++++++++++++++++++- 16 files changed, 1065 insertions(+), 716 deletions(-) create mode 100644 lizmap/definitions/lizmap_cloud.py delete mode 100644 lizmap/definitions/warnings.py delete mode 100644 lizmap/models/__init__.py delete mode 100644 lizmap/models/check_project.py delete mode 100644 lizmap/test/manuel_table_checks.py diff --git a/lizmap/definitions/lizmap_cloud.py b/lizmap/definitions/lizmap_cloud.py new file mode 100644 index 00000000..d6ea29a8 --- /dev/null +++ b/lizmap/definitions/lizmap_cloud.py @@ -0,0 +1,10 @@ +__copyright__ = 'Copyright 2023, 3Liz' +__license__ = 'GPL version 3' +__email__ = 'info@3liz.org' + +CLOUD_DOMAIN = 'lizmap.com' +CLOUD_NAME = 'Lizmap Cloud' +CLOUD_MAX_PARENT_FOLDER = 2 # TODO Check COG, is-it 3 ? + +CLOUD_ONLINE_URL = 'https://docs.lizmap.cloud' +CLOUD_ONLINE_LANGUAGES = ('en', 'fr') diff --git a/lizmap/definitions/online_help.py b/lizmap/definitions/online_help.py index acf8aa7a..92a08d73 100644 --- a/lizmap/definitions/online_help.py +++ b/lizmap/definitions/online_help.py @@ -7,13 +7,15 @@ from qgis.core import QgsSettings from qgis.PyQt.QtCore import QLocale, QUrl +from lizmap.definitions.lizmap_cloud import ( + CLOUD_ONLINE_LANGUAGES, + CLOUD_ONLINE_URL, +) + DOMAIN = 'https://docs.lizmap.com' VERSION = 'current' ONLINE_HELP_LANGUAGES = ('en', 'es', 'it', 'ja', 'pt', 'fi', 'fr') -CLOUD = 'https://docs.lizmap.cloud' -CLOUD_HELP_LANGUAGES = ('en', 'fr') - def current_locale() -> str: """ Get the main language, with 2 characters only. """ @@ -25,9 +27,9 @@ def current_locale() -> str: def online_cloud_help(page: str = '') -> QUrl: """ Online help URL according to locale and version. """ locale = current_locale() - if locale not in CLOUD_HELP_LANGUAGES: + if locale not in CLOUD_ONLINE_LANGUAGES: locale = 'en' - return QUrl(f"{CLOUD}/{locale}/{page}") + return QUrl(f"{CLOUD_ONLINE_URL}/{locale}/{page}") def online_lwc_help(page: str = '', version=VERSION) -> QUrl: diff --git a/lizmap/definitions/qgis_settings.py b/lizmap/definitions/qgis_settings.py index 33c48462..e64fdb3c 100644 --- a/lizmap/definitions/qgis_settings.py +++ b/lizmap/definitions/qgis_settings.py @@ -17,10 +17,10 @@ def key(cls, key): return KEY + '/' + key PreventEcw = 'prevent_ecw' - PreventPgAuthId = 'prevent_pg_auth_id' + PreventPgAuthDb = 'prevent_pg_auth_db' PreventPgService = 'prevent_pg_service' ForcePgUserPass = 'force_pg_user_password' - PreventNetworkDrive = 'prevent_network_drive' + PreventDrive = 'prevent_drive' AllowParentFolder = 'allow_parent_folder' NumberParentFolder = 'number_parent_folder' BeginnerMode = 'beginner_mode' diff --git a/lizmap/definitions/warnings.py b/lizmap/definitions/warnings.py deleted file mode 100644 index b137d81e..00000000 --- a/lizmap/definitions/warnings.py +++ /dev/null @@ -1,15 +0,0 @@ -__copyright__ = 'Copyright 2022, 3Liz' -__license__ = 'GPL version 3' -__email__ = 'info@3liz.org' - - -from enum import Enum, unique - - -@unique -class Warnings(Enum): - OgcNotValid = 'ogc_not_valid' - UseLayerIdAsName = 'use_layer_id_as_name' - SaasLizmapCloud = 'saas_lizmap_cloud_invalid' - InvalidFieldType = 'invalid_field_type' - DuplicatedLayersWithFilters = 'duplicated_layers_with_filters' diff --git a/lizmap/dialogs/main.py b/lizmap/dialogs/main.py index c2341b6c..ca48a6f8 100755 --- a/lizmap/dialogs/main.py +++ b/lizmap/dialogs/main.py @@ -28,22 +28,23 @@ ) from qgis.utils import OverrideCursor, iface +from lizmap.definitions.lizmap_cloud import CLOUD_MAX_PARENT_FOLDER, CLOUD_NAME from lizmap.definitions.qgis_settings import Settings from lizmap.log_panel import LogPanel -from lizmap.models.check_project import TableCheck from lizmap.project_checker_tools import ( ALLOW_PARENT_FOLDER, FORCE_LOCAL_FOLDER, FORCE_PG_USER_PASS, PREVENT_AUTH_DB, PREVENT_ECW, - PREVENT_NETWORK_DRIVE, + PREVENT_OTHER_DRIVE, PREVENT_SERVICE, project_trust_layer_metadata, simplify_provider_side, use_estimated_metadata, ) -from lizmap.saas import SAAS_MAX_PARENT_FOLDER, SAAS_NAME, fix_ssl +from lizmap.saas import fix_ssl +from lizmap.widgets.check_project import Checks, TableCheck try: from qgis.PyQt.QtWebKitWidgets import QWebView @@ -177,7 +178,7 @@ def __init__(self, parent=None, is_dev_version=True): self.button_use_estimated_md.setIcon(QIcon(":images/themes/default/mIconPostgis.svg")) self.button_trust_project.clicked.connect(self.fix_project_trust) - # self.button_trust_project.setIcon(QIcon(":images/themes/default/mIconPostgis.svg")) + self.button_trust_project.setIcon(QIcon(':/images/themes/default/mIconQgsProjectFile.svg')) self.button_simplify_geom.clicked.connect(self.fix_simplify_geom_provider) self.button_simplify_geom.setIcon(QIcon(":images/themes/default/mIconPostgis.svg")) @@ -231,13 +232,13 @@ def __init__(self, parent=None, is_dev_version=True): ) self.label_file_action.setOpenExternalLinks(True) - self.radio_beginner.setToolTip( + self.radio_beginner.setToolTip(tr( 'If one safeguard is not OK, the Lizmap configuration file is not going to be generated.' - ) - self.radio_normal.setToolTip( + )) + self.radio_normal.setToolTip(tr( 'If one safeguard is not OK, only a warning will be displayed, not blocking the saving of the Lizmap ' 'configuration file.' - ) + )) self.radio_force_local_folder.setText(FORCE_LOCAL_FOLDER) self.radio_force_local_folder.setToolTip(tr( @@ -247,7 +248,8 @@ def __init__(self, parent=None, is_dev_version=True): self.radio_allow_parent_folder.setToolTip(tr( 'Files can be located in a parent folder from {}, up to the setting below.' ).format(self.project.absolutePath())) - self.safe_network_drive.setText(PREVENT_NETWORK_DRIVE) + + self.safe_other_drive.setText(PREVENT_OTHER_DRIVE) self.safe_pg_service.setText(PREVENT_SERVICE) self.safe_pg_auth_db.setText(PREVENT_AUTH_DB) self.safe_pg_user_password.setText(FORCE_PG_USER_PASS) @@ -275,27 +277,34 @@ def __init__(self, parent=None, is_dev_version=True): self.safe_number_parent.setValue(QgsSettings().value(Settings.key(Settings.NumberParentFolder), type=int)) self.safe_number_parent.valueChanged.connect(self.save_settings) - # Network drive - self.safe_network_drive.setChecked(QgsSettings().value(Settings.key(Settings.PreventNetworkDrive), type=bool)) - self.safe_network_drive.toggled.connect(self.save_settings) + # Other drive + self.safe_other_drive.setChecked(QgsSettings().value(Settings.key(Settings.PreventDrive), type=bool)) + self.safe_other_drive.toggled.connect(self.save_settings) + self.safe_other_drive.setToolTip(Checks.PreventDrive.description) # PG Service self.safe_pg_service.setChecked(QgsSettings().value(Settings.key(Settings.PreventPgService), type=bool)) self.safe_pg_service.toggled.connect(self.save_settings) + self.safe_pg_service.setToolTip(Checks.PgService.description) # PG Auth DB - self.safe_pg_auth_db.setChecked(QgsSettings().value(Settings.key(Settings.PreventPgAuthId), type=bool)) + self.safe_pg_auth_db.setChecked(QgsSettings().value(Settings.key(Settings.PreventPgAuthDb), type=bool)) self.safe_pg_auth_db.toggled.connect(self.save_settings) + self.safe_pg_auth_db.setToolTip(Checks.AuthenticationDb.description) # User password self.safe_pg_user_password.setChecked(QgsSettings().value(Settings.key(Settings.ForcePgUserPass), type=bool)) self.safe_pg_user_password.toggled.connect(self.save_settings) + self.safe_pg_user_password.setToolTip(Checks.PgForceUserPass.description) # ECW self.safe_ecw.setChecked(QgsSettings().value(Settings.key(Settings.PreventEcw), type=bool)) self.safe_ecw.toggled.connect(self.save_settings) + self.safe_ecw.setToolTip(Checks.PreventEcw.description) - self.label_safe_lizmap_cloud.setText(tr("Some safe guards are overridden by {}.").format(SAAS_NAME)) + self.label_safe_lizmap_cloud.setText(tr( + "Some safeguards are overridden by {host}. Even in 'normal' mode, some safeguards are becoming 'blocking' " + "with a {host} instance.").format(host=CLOUD_NAME)) msg = ( ''.format( max_parent=tr("Maximum of parent folder {} : {}").format( - SAAS_MAX_PARENT_FOLDER, relative_path(SAAS_MAX_PARENT_FOLDER)), - network=PREVENT_NETWORK_DRIVE, + CLOUD_MAX_PARENT_FOLDER, relative_path(CLOUD_MAX_PARENT_FOLDER)), + network=PREVENT_OTHER_DRIVE, auth_db=PREVENT_AUTH_DB, user_pass=FORCE_PG_USER_PASS, ecw=PREVENT_ECW, @@ -315,21 +324,11 @@ def __init__(self, parent=None, is_dev_version=True): self.label_safe_lizmap_cloud.setToolTip(msg) self.table_checks.setup() + css_path = resources_path('css', 'log.css') + with open(css_path, encoding='utf8') as f: + css = f.read() + self.html_help.document().setDefaultStyleSheet(css) - # self.table_checks.setSelectionMode(QAbstractItemView.SingleSelection) - # self.table_checks.setEditTriggers(QAbstractItemView.NoEditTriggers) - # self.table_checks.setSelectionBehavior(QAbstractItemView.SelectRows) - # self.table_checks.setAlternatingRowColors(True) - # self.table_checks.horizontalHeader().setStretchLastSection(True) - # self.table_checks.horizontalHeader().setVisible(True) - # print("BOB") - # - # self.table_checks.setColumnCount(len(Headers)) - # for i, header in enumerate(Headers): - # column = QTableWidgetItem(header.label) - # column.setToolTip(header.tooltip) - # self.table_checks.setHorizontalHeaderItem(i, column) - # print(i) @property def check_results(self) -> TableCheck: return self.table_checks @@ -908,7 +907,7 @@ def radio_mode_normal_toggled(self): widgets = ( self.group_file_layer, self.safe_number_parent, - self.safe_network_drive, + self.safe_other_drive, self.safe_pg_service, self.safe_pg_auth_db, self.safe_pg_user_password, @@ -924,9 +923,9 @@ def save_settings(self): QgsSettings().setValue(Settings.key(Settings.BeginnerMode), not self.radio_normal.isChecked()) QgsSettings().setValue(Settings.key(Settings.AllowParentFolder), self.radio_allow_parent_folder.isChecked()) QgsSettings().setValue(Settings.key(Settings.NumberParentFolder), self.safe_number_parent.value()) - QgsSettings().setValue(Settings.key(Settings.PreventNetworkDrive), self.safe_network_drive.isChecked()) + QgsSettings().setValue(Settings.key(Settings.PreventDrive), self.safe_other_drive.isChecked()) QgsSettings().setValue(Settings.key(Settings.PreventPgService), self.safe_pg_service.isChecked()) - QgsSettings().setValue(Settings.key(Settings.PreventPgAuthId), self.safe_pg_auth_db.isChecked()) + QgsSettings().setValue(Settings.key(Settings.PreventPgAuthDb), self.safe_pg_auth_db.isChecked()) QgsSettings().setValue(Settings.key(Settings.ForcePgUserPass), self.safe_pg_user_password.isChecked()) QgsSettings().setValue(Settings.key(Settings.PreventEcw), self.safe_ecw.isChecked()) diff --git a/lizmap/models/__init__.py b/lizmap/models/__init__.py deleted file mode 100644 index d4bbd6ea..00000000 --- a/lizmap/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__copyright__ = 'Copyright 2023, 3Liz' -__license__ = 'GPL version 3' -__email__ = 'info@3liz.org' diff --git a/lizmap/models/check_project.py b/lizmap/models/check_project.py deleted file mode 100644 index 2ec4ad09..00000000 --- a/lizmap/models/check_project.py +++ /dev/null @@ -1,222 +0,0 @@ -__copyright__ = 'Copyright 2023, 3Liz' -__license__ = 'GPL version 3' -__email__ = 'info@3liz.org' - -from enum import Enum - -from qgis.core import QgsMarkerSymbol, QgsSymbolLayerUtils -from qgis.PyQt.QtCore import QSize, Qt -from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import ( - QAbstractItemView, - QTableWidget, - QTableWidgetItem, -) - -from lizmap.qgis_plugin_tools.tools.i18n import tr - - -class Header: - def __init__(self, label, tooltip): - self.label = label - self.tooltip = tooltip - - -class Headers(Header, Enum): - Severity = tr('Severity'), tr("Severity of the error") - Level = tr('Level'), tr("Level of the error") - Object = tr('Object name'), tr("Name of the object bringing the issue") - Name = tr('Name'), tr('Name of the error') - Error = tr('Error'), tr('Description of the error') - - -class Severity: - def __init__(self, data, label, tooltip, color): - self.data = data - self.label = label - self.color = color - self.tooltip = tooltip - - def marker(self) -> QIcon: - pixmap = QgsSymbolLayerUtils.symbolPreviewPixmap( - QgsMarkerSymbol.createSimple( - { - "name": "circle", - "color": self.color, - "size": "2", - } - ), - QSize(16, 16) - ) - return QIcon(pixmap) - - -class Severities(Severity, Enum): - Blocking = 'blocking', tr('Blocking'), tr('This is blocking the CFG file'), 'green' - Important = 'important', tr('Important'), tr('This is important to fix, to improve performance'), 'red' - Normal = 'normal', tr('Normal'), tr('This would be nice to have look'), 'blue' - Low = 'low', tr('Low'), tr('Nice to do'), 'green' - - -class Level: - def __init__(self, data: str, label: str, tooltip: str, icon: QIcon): - self.data = data - self.label = label - self.icon = icon - self.tooltip = tooltip - - -class Levels: - GlobalConfig = Level( - 'global', - tr('Global'), - tr('Issue in the global configuration'), - QIcon(':/images/themes/default/console/iconSettingsConsole.svg'), - ) - Project = Level( - 'project', - tr('Project'), - tr('Issue at the project level, usually in Project properties dialog'), - QIcon(':/images/themes/default/mIconQgsProjectFile.svg'), - ) - Layer = Level( - 'layer', - tr('Layer'), - tr('Issue at the layer level'), - QIcon(':/images/themes/default/algorithms/mAlgorithmMergeLayers.svg'), - ) - - -class Check: - def __init__(self, title: str, description: str, tooltip: str, level: Level, severity: Severity): - self.title = title - self.description = description - self.tooltip = tooltip - self.level = level - self.severity = severity - - -class Checks(Check, Enum): - EstimatedMetadata = ( - tr('Estimated metadata'), - tr("Estimated metadata is missing on the layer"), - '', - Levels.Layer, - Severities.Blocking, - ) - DuplicatedLayerNameOrGroup = ( - tr('Duplicated layer name or group'), - tr("It's not possible to store all the Lizmap configuration for these layer(s) or group(s)."), - '', - Levels.Project, - Severities.Blocking, - ) - - -class Error: - def __init__(self, identifier: str, check: Check): - self.identifier = identifier - self.check = check - - -class TableCheck(QTableWidget): - def setup(self): - self.setSelectionMode(QAbstractItemView.SingleSelection) - self.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.setSelectionBehavior(QAbstractItemView.SelectRows) - self.setAlternatingRowColors(True) - self.horizontalHeader().setStretchLastSection(True) - self.horizontalHeader().setVisible(True) - - self.setColumnCount(len(Headers)) - for i, header in enumerate(Headers): - column = QTableWidgetItem(header.label) - column.setToolTip(header.tooltip) - self.setHorizontalHeaderItem(i, column) - - def add_error(self, error: Error): - self.add_row( - error.check.severity, - error.check.level, - error.identifier, - error.check.title, - error.check.description, - error.check.tooltip - ) - - def add_row(self, severity: Severity, level: Level, identifier: str, error_name, error_description, error_help): - row = self.rowCount() - self.setRowCount(row + 1) - - item = QTableWidgetItem(severity.label) - item.setData(Qt.UserRole, severity.data) - item.setToolTip(severity.tooltip) - item.setIcon(severity.marker()) - self.setItem(row, 0, item) - - item = QTableWidgetItem(level.label) - item.setData(Qt.UserRole, level.data) - item.setToolTip(level.tooltip) - item.setIcon(level.icon) - self.setItem(row, 1, item) - - item = QTableWidgetItem(identifier) - item.setData(Qt.UserRole, identifier) - self.setItem(row, 2, item) - - item = QTableWidgetItem(error_name) - item.setData(Qt.UserRole, error_name) - self.setItem(row, 3, item) - - item = QTableWidgetItem(error_description) - item.setData(Qt.UserRole, error_description) - item.setToolTip(error_help) - self.setItem(row, 4, item) - -# class TableModel(QAbstractTableModel): -# -# def __init__(self, parent=None, *args, **kwargs): -# super().__init__(parent, *args, **kwargs) -# self.rows = [] -# -# def add_row(self, name, date, interest): -# self.rows.append((name, date, interest)) -# -# index = self.createIndex(0,0) -# self.dataChanged.emit(index, index, [Qt.DisplayRole]) -# # print(self.insertRow(0)) -# # print(self.rowCount()) -# # self.setData(self.createIndex(self.rowCount(), 0), name, Qt.EditRole) -# # self.setData(self.createIndex(self.rowCount(), 1), date, Qt.EditRole) -# # self.setData(self.createIndex(self.rowCount(), 2), interest, Qt.EditRole) -# -# def rowCount(self, parent=None): -# return len(self.rows) -# -# def columnCount(self, parent): -# return len(Headers) -# def data(self, index, role): -# if role != Qt.DisplayRole: -# return QVariant() -# -# if index.column() >= len(self.rows[index.row()]): -# return 'a' -# -# # What's the value of the cell at the given index? -# return self.rows[index.row()][index.column()] -# -# def headerData(self, section, orientation, role): -# if orientation != Qt.Horizontal: -# return '' -# -# header: Header = list(Headers)[section] -# -# if role == Qt.DisplayRole: -# return header.label -# -# if role == Qt.ToolTipRole: -# return header.tooltip diff --git a/lizmap/plugin.py b/lizmap/plugin.py index d2d5dfd1..e6a4cf28 100755 --- a/lizmap/plugin.py +++ b/lizmap/plugin.py @@ -86,6 +86,7 @@ from lizmap.definitions.filter_by_login import FilterByLoginDefinitions from lizmap.definitions.filter_by_polygon import FilterByPolygonDefinitions from lizmap.definitions.layouts import LayoutsDefinitions +from lizmap.definitions.lizmap_cloud import CLOUD_MAX_PARENT_FOLDER, CLOUD_NAME from lizmap.definitions.locate_by_layer import LocateByLayerDefinitions from lizmap.definitions.online_help import ( MAPPING_INDEX_DOC, @@ -95,7 +96,6 @@ from lizmap.definitions.qgis_settings import Settings from lizmap.definitions.time_manager import TimeManagerDefinitions from lizmap.definitions.tooltip import ToolTipDefinitions -from lizmap.definitions.warnings import Warnings from lizmap.dialogs.html_editor import HtmlEditorDialog from lizmap.dialogs.html_maptip import HtmlMapTipDialog from lizmap.dialogs.lizmap_popup import LizmapPopupDialog @@ -117,26 +117,21 @@ from lizmap.ogc_project_validity import OgcProjectValidity from lizmap.project_checker_tools import ( ALLOW_PARENT_FOLDER, + FORCE_LOCAL_FOLDER, FORCE_PG_USER_PASS, PREVENT_AUTH_DB, PREVENT_ECW, - PREVENT_NETWORK_DRIVE, + PREVENT_OTHER_DRIVE, PREVENT_SERVICE, - auto_generated_primary_key_field, duplicated_layer_name_or_group, duplicated_layer_with_filter, - invalid_int8_primary_key, + project_invalid_pk, project_safeguards_checks, project_trust_layer_metadata, simplify_provider_side, use_estimated_metadata, ) -from lizmap.saas import ( - SAAS_MAX_PARENT_FOLDER, - SAAS_NAME, - check_project_ssl_postgis, - is_lizmap_cloud, -) +from lizmap.saas import check_project_ssl_postgis, is_lizmap_cloud from lizmap.table_manager.base import TableManager from lizmap.table_manager.dataviz import TableManagerDataviz from lizmap.table_manager.layouts import TableManagerLayouts @@ -180,6 +175,7 @@ ) from lizmap.tooltip import Tooltip from lizmap.version_checker import VersionChecker +from lizmap.widgets.check_project import Checks, Error, Severities, SourceLayer if qgis_version() >= 31400: from qgis.core import QgsProjectServerValidator @@ -216,9 +212,9 @@ def __init__(self, iface): if prevent_ecw is None: QgsSettings().setValue(Settings.key(Settings.PreventEcw), True) - prevent_auth_id = QgsSettings().value(Settings.key(Settings.PreventPgAuthId), defaultValue=None) + prevent_auth_id = QgsSettings().value(Settings.key(Settings.PreventPgAuthDb), defaultValue=None) if prevent_auth_id is None: - QgsSettings().setValue(Settings.key(Settings.PreventPgAuthId), True) + QgsSettings().setValue(Settings.key(Settings.PreventPgAuthDb), True) prevent_service = QgsSettings().value(Settings.key(Settings.PreventPgService), defaultValue=None) if prevent_service is None: @@ -228,9 +224,9 @@ def __init__(self, iface): if force_pg_user_pass is None: QgsSettings().setValue(Settings.key(Settings.ForcePgUserPass), True) - prevent_network_drive = QgsSettings().value(Settings.key(Settings.PreventNetworkDrive), defaultValue=None) - if prevent_network_drive is None: - QgsSettings().setValue(Settings.key(Settings.PreventNetworkDrive), True) + prevent_other_drive = QgsSettings().value(Settings.key(Settings.PreventDrive), defaultValue=None) + if prevent_other_drive is None: + QgsSettings().setValue(Settings.key(Settings.PreventDrive), True) allow_parent_folder = QgsSettings().value(Settings.key(Settings.AllowParentFolder), defaultValue=None) if allow_parent_folder is None: @@ -1006,7 +1002,7 @@ def initGui(self): # noinspection PyUnresolvedReferences self.help_action.triggered.connect(self.show_help) - self.help_action_cloud = QAction(icon, SAAS_NAME, self.iface.mainWindow()) + self.help_action_cloud = QAction(icon, CLOUD_NAME, self.iface.mainWindow()) self.iface.pluginHelpMenu().addAction(self.help_action_cloud) # noinspection PyUnresolvedReferences self.help_action.triggered.connect(self.show_help_cloud) @@ -2885,39 +2881,17 @@ def project_config_file( if next_version != 'next': current_version = next_version - warnings = [] - log_index_panel = self.dlg.mOptionsListWidget.count() - 2 - settings_panel_name = self.dlg.mOptionsListWidget.item(self.dlg.mOptionsListWidget.count() - 1).text() - - # If the log panel must be shown to the user - # Either a warning or an error - show_log_panel = False - warning_suggest = tr('This issue not blocking the generation of the Lizmap configuration file.') - # If the user must fix some issues, the CFG will not be generated - # But with QGIS desktop < 3.22, it's not possible to do it automatically - error_cfg_saving = False - error_cfg_suggest = tr('Or use the automatic fixing button.') - error_cfg_message = tr( - "This issue must be fixed, the configuration is not going to be saved. You must visit the '{}' panel to " - "click the auto-fix button for layers currently loaded in this project." - ).format(settings_panel_name) - # Temporary disabled when used in production - qgis_min_required_not_prod_ready = 32200 if self.is_dev_version else 39900 + duplicated_in_cfg = duplicated_layer_name_or_group(self.project) + for name, count in duplicated_in_cfg.items(): + if count >= 2: + source = '"{}" → "'.format(name) + tr("count {} layers").format(count) + self.dlg.check_results.add_error(Error(source, Checks.DuplicatedLayerNameOrGroup)) # Layer ID as short name if lwc_version >= LwcVersions.Lizmap_3_6: use_layer_id, _ = self.project.readEntry('WMSUseLayerIDs', '/') if to_bool(use_layer_id, False): - show_log_panel = True - self.dlg.log_panel.append(tr('Use layer IDs as name'), Html.H2) - self.dlg.log_panel.append(warning_suggest, Html.P) - self.dlg.log_panel.append(tr( - "Since Lizmap Web Client 3.6, it's not possible anymore to use the option 'Use layer IDs " - "as name' in the project properties dialog, QGIS server tab, then WMS capabilities." - ), Html.P) - self.dlg.log_panel.append(tr( - "Please uncheck this checkbox and re-save the Lizmap configuration file."), Html.P) - warnings.append(Warnings.UseLayerIdAsName.value) + self.dlg.check_results.add_error(Error(Path(self.project.fileName()).name, Checks.WmsUseLayerIds)) target_status = self.dlg.server_combo.currentData(ServerComboData.LwcBranchStatus.value) if not target_status: @@ -2929,10 +2903,10 @@ def project_config_file( # Global checks config prevent_ecw = QgsSettings().value(Settings.key(Settings.PreventEcw), True, bool) - prevent_auth_id = QgsSettings().value(Settings.key(Settings.PreventPgAuthId), True, bool) + prevent_auth_id = QgsSettings().value(Settings.key(Settings.PreventPgAuthDb), True, bool) prevent_service = QgsSettings().value(Settings.key(Settings.PreventPgService), True, bool) force_pg_user_pass = QgsSettings().value(Settings.key(Settings.ForcePgUserPass), True, bool) - prevent_network_drive = QgsSettings().value(Settings.key(Settings.PreventNetworkDrive), True, bool) + prevent_other_drive = QgsSettings().value(Settings.key(Settings.PreventDrive), True, bool) allow_parent_folder = QgsSettings().value(Settings.key(Settings.AllowParentFolder), False, bool) count_parent_folder = QgsSettings().value(Settings.key(Settings.NumberParentFolder), 2, int) @@ -2944,9 +2918,9 @@ def project_config_file( prevent_ecw = True prevent_auth_id = True force_pg_user_pass = True - prevent_network_drive = True - if count_parent_folder > SAAS_MAX_PARENT_FOLDER: - count_parent_folder = SAAS_MAX_PARENT_FOLDER + prevent_other_drive = True + if count_parent_folder > CLOUD_MAX_PARENT_FOLDER: + count_parent_folder = CLOUD_MAX_PARENT_FOLDER # prevent_service = False We encourage service # allow_parent_folder = False Of course we can @@ -2959,66 +2933,65 @@ def project_config_file( summary.append(PREVENT_SERVICE) if force_pg_user_pass: summary.append(FORCE_PG_USER_PASS) - if prevent_network_drive: - summary.append(PREVENT_NETWORK_DRIVE) + if prevent_other_drive: + summary.append(PREVENT_OTHER_DRIVE) if allow_parent_folder: summary.append(ALLOW_PARENT_FOLDER + " : " + tr("{} folder(s)").format(count_parent_folder)) + else: + summary.append(FORCE_LOCAL_FOLDER) parent_folder = relative_path(count_parent_folder) - results, more = project_safeguards_checks( + results = project_safeguards_checks( self.project, prevent_ecw=prevent_ecw, prevent_auth_id=prevent_auth_id, prevent_service=prevent_service, force_pg_user_pass=force_pg_user_pass, - prevent_network_drive=prevent_network_drive, + prevent_other_drive=prevent_other_drive, allow_parent_folder=allow_parent_folder, parent_folder=parent_folder, lizmap_cloud=lizmap_cloud, ) - if len(results): - show_log_panel = True - warnings.append(Warnings.SaasLizmapCloud.value) - self.dlg.log_panel.append(tr('Some safeguards are not compatible'), Html.H2) - # Let's show a summary - if lizmap_cloud: - self.dlg.log_panel.append(tr("According to global settings, overriden then by {} :").format(SAAS_NAME), Html.P) - else: - self.dlg.log_panel.append(tr("According to global settings"), Html.P) - - self.dlg.log_panel.start_table() - for i, rule in enumerate(summary): - self.dlg.log_panel.add_row(i) - self.dlg.log_panel.append(rule, Html.Td) - self.dlg.log_panel.end_row() - - self.dlg.log_panel.append("
") + # Let's show a summary + if lizmap_cloud: + self.dlg.log_panel.append( + tr("According to global settings, overriden then by {} :").format(CLOUD_NAME), Html.P) + else: + self.dlg.log_panel.append(tr("According to global settings"), Html.P) - if beginner_mode: - self.dlg.log_panel.append(tr( - "These issues below are blocking saving the Lizmap configuration file in the 'Beginner' mode." - ), Html.P, level=Qgis.Critical) - else: - self.dlg.log_panel.append(warning_suggest, Html.P) + self.dlg.log_panel.start_table() + for i, rule in enumerate(summary): + self.dlg.log_panel.add_row(i) + self.dlg.log_panel.append(rule, Html.Td) + self.dlg.log_panel.end_row() + self.dlg.log_panel.end_table() - self.dlg.log_panel.append("
") + self.dlg.log_panel.append("
") - self.dlg.log_panel.start_table() - for i, error in enumerate(results.values()): - self.dlg.log_panel.add_row(i) - self.dlg.log_panel.append(error, Html.Td) - self.dlg.log_panel.end_row() + for layer, error in results.items(): - self.dlg.log_panel.end_table() - self.dlg.log_panel.append("
") + # Severity depends on beginner mode + severity = Severities.Blocking if beginner_mode else Severities.Important + # But override severities for Lizmap Cloud + # Because even with a 'normal' user, it won't work + override = ( + Checks.PreventEcw, Checks.PgForceUserPass, Checks.AuthenticationDb, Checks.PreventDrive) + if error in override: + severity = Severities.Blocking - if more: - self.dlg.log_panel.append(more) - self.dlg.log_panel.append("
") + self.dlg.check_results.add_error( + Error( + layer.layer_name, + error, + source_type=SourceLayer(layer.layer_name, layer.layer_id), + ), + lizmap_cloud=lizmap_cloud, + severity=severity, + ) + if results: if beginner_mode: - error_cfg_saving = True self.dlg.log_panel.append(tr( "The process is stopping, the CFG file is not going to be generated because some safeguards " "are not compatible and you are using the 'Beginner' mode. Either fix these issues or switch " @@ -3033,168 +3006,83 @@ def project_config_file( if check_server: error, message = check_project_ssl_postgis(self.project) - if error: - self.dlg.log_panel.append(tr('SSL connections to a PostgreSQL database'), Html.H2) - self.dlg.log_panel.append(tr( - "Connections to a PostgreSQL database hosted on {} must use a SSL secured connection." - ).format(SAAS_NAME), Html.P) - self.dlg.log_panel.append(tr( - "In the plugin, then in the '{}' panel, there is a helper to change the datasource of layers in " - "the current project only. It works only with minimum QGIS 3.22." - ).format(settings_panel_name), Html.P) - self.dlg.log_panel.append(tr( - "You must still edit your global PostgreSQL connection to allow SSL, it will take effect only " - "on newly added layer into a project." - ), Html.P) - self.dlg.log_panel.append(message, Html.P) - show_log_panel = True - - if Qgis.QGIS_VERSION_INT >= qgis_min_required_not_prod_ready: - error_cfg_saving = True - self.dlg.log_panel.append(error_cfg_suggest, Html.P) - self.dlg.log_panel.append(error_cfg_message, Html.P, level=Qgis.Critical) - self.dlg.enabled_ssl_button(True) - else: - self.dlg.log_panel.append(warning_suggest, Html.P) - - autogenerated_keys = {} - int8 = [] - for layer in self.project.mapLayers().values(): - - if not isinstance(layer, QgsVectorLayer): - continue - - result, field = auto_generated_primary_key_field(layer) - if result: - if field not in autogenerated_keys.keys(): - autogenerated_keys[field] = [] - - autogenerated_keys[field].append(layer.name()) - - if invalid_int8_primary_key(layer): - int8.append(layer.name()) - - if autogenerated_keys or int8: - show_log_panel = True - warnings.append(Warnings.InvalidFieldType.value) - self.dlg.log_panel.append(tr('Some fields are invalid for QGIS server'), Html.H2) - self.dlg.log_panel.append(warning_suggest, Html.P) - - for field, layers in autogenerated_keys.items(): - # field can be "tid", "ctid" etc - self.dlg.log_panel.append(tr( - "These layers don't have a proper primary key in the database. So QGIS Desktop tried to set a " - "temporary field called '{}' to be a unique identifier. On QGIS Server, this will bring issues." - ).format(field), Html.P) - self.dlg.log_panel.append("
") - layers.sort() - self.dlg.log_panel.start_table() - for i, layer_name in enumerate(layers): - self.dlg.log_panel.add_row(i) - self.dlg.log_panel.append(layer_name, Html.Td) - self.dlg.log_panel.end_row() - - self.dlg.log_panel.end_table() - - if int8: - int8.sort() - self.dlg.log_panel.append(tr( - "The primary key has been detected as a bigint (integer8) for your layer :"), Html.P) - self.dlg.log_panel.start_table() - for i, layer_name in enumerate(int8): - self.dlg.log_panel.add_row(i) - self.dlg.log_panel.append(layer_name, Html.Td) - self.dlg.log_panel.end_row() - self.dlg.log_panel.end_table() - self.dlg.log_panel.append("
") - - if autogenerated_keys or int8: - self.dlg.log_panel.append(tr( - "We highly recommend you to set a proper integer field as a primary key, but neither a bigint nor " - "an integer8."), Html.P) - self.dlg.log_panel.append(tr( - "The process is continuing but expect these layers to have some issues with some tools in " - "Lizmap Web Client: zoom to feature, filtering…" - ), style=Html.P) + for layer in error: + self.dlg.check_results.add_error( + Error( + layer.layer_name, + Checks.SSLConnection, + source_type=SourceLayer(layer.layer_name, layer.layer_id), + ) + ) + self.dlg.enabled_ssl_button(True) + + autogenerated_keys, int8 = project_invalid_pk(self.project) + for layer in autogenerated_keys: + self.dlg.check_results.add_error( + Error( + layer.layer_name, + Checks.MissingPk, + source_type=SourceLayer(layer.layer_name, layer.layer_id), + ) + ) + for layer in int8: + self.dlg.check_results.add_error( + Error( + layer.layer_name, + Checks.PkInt8, + source_type=SourceLayer(layer.layer_name, layer.layer_id), + ) + ) if lwc_version >= LwcVersions.Lizmap_3_7: text = duplicated_layer_with_filter(self.project) if text: self.dlg.log_panel.append(tr('Optimisation about the legend'), Html.H2) - self.dlg.log_panel.append(warning_suggest, Html.P) + (self.dlg.log_panel.append + (tr('This issue not blocking the generation of the Lizmap configuration file.'), Html.P)) self.dlg.log_panel.append(text, style=Html.P) - warnings.append(Warnings.DuplicatedLayersWithFilters.value) results = simplify_provider_side(self.project) - if len(results): - self.dlg.log_panel.append(tr('Simplify geometry on the provider side'), Html.H2) - self.dlg.log_panel.append(tr( - 'These PostgreSQL vector layers can have the simplification on the provider side') + ':', Html.P) - self.dlg.log_panel.start_table() - for i, layer in enumerate(results): - self.dlg.log_panel.add_row(i) - self.dlg.log_panel.append(layer, Html.Td) - self.dlg.log_panel.end_row() - self.dlg.log_panel.end_table() - self.dlg.log_panel.append(tr( - 'Visit the layer properties, then in the "Rendering" tab to enable it.'), Html.P) - if Qgis.QGIS_VERSION_INT >= qgis_min_required_not_prod_ready: - self.dlg.log_panel.append(error_cfg_message, Html.P, level=Qgis.Critical) - show_log_panel = True - error_cfg_saving = True + for layer in results: + self.dlg.check_results.add_error( + Error( + layer.layer_name, + Checks.SimplifyGeometry, + source_type=SourceLayer(layer.layer_name, layer.layer_id), + ) + ) self.dlg.enabled_simplify_geom(True) results = use_estimated_metadata(self.project) - if len(results): - self.dlg.log_panel.append(tr('Estimated metadata'), Html.H2) - self.dlg.log_panel.append(tr( - 'These PostgreSQL layers can have the use estimated metadata option enabled') + ':', Html.P) - self.dlg.log_panel.start_table() - for i, layer in enumerate(results): - self.dlg.log_panel.add_row(i) - self.dlg.log_panel.append(layer, Html.Td) - self.dlg.log_panel.end_row() - self.dlg.log_panel.end_table() - self.dlg.log_panel.append(tr( - 'Edit your PostgreSQL connection to enable this option, then change the datasource by right clicking ' - 'on each layer above, then click "Change datasource" in the menu. Finally reselect your layer in the ' - 'new dialog.'), Html.P) - if Qgis.QGIS_VERSION_INT >= qgis_min_required_not_prod_ready: - self.dlg.log_panel.append(error_cfg_suggest, Html.P) - self.dlg.log_panel.append(error_cfg_message, Html.P, level=Qgis.Critical) - show_log_panel = True - error_cfg_saving = True - self.dlg.enabled_estimated_md_button(True) - else: - self.dlg.log_panel.append(warning_suggest, Html.P) + for layer in results: + self.dlg.check_results.add_error( + Error( + layer.layer_name, + Checks.EstimatedMetadata, + source_type=SourceLayer(layer.layer_name, layer.layer_id), + ) + ) + self.dlg.enabled_estimated_md_button(True) - if not project_trust_layer_metadata(self.project) and Qgis.QGIS_VERSION_INT >= qgis_min_required_not_prod_ready: - self.dlg.log_panel.append(tr('Trust project metadata'), Html.H2) - self.dlg.log_panel.append(tr( - 'The project does not have the "Trust project metadata" enabled at the project level'), Html.P) - self.dlg.log_panel.append(tr( - 'In the project properties → Data sources → at the bottom, there is a checkbox to trust the project ' - 'when the layer has no metadata.'), Html.P) - self.dlg.log_panel.append(error_cfg_suggest, Html.P) - self.dlg.log_panel.append(error_cfg_message, Html.P, level=Qgis.Critical) - show_log_panel = True - error_cfg_saving = True + if not project_trust_layer_metadata(self.project): + self.dlg.check_results.add_error(Error(Path(self.project.fileName()).name, Checks.TrustProject)) self.dlg.enabled_trust_project(True) - if with_gui and show_log_panel: - self.dlg.mOptionsListWidget.setCurrentRow(log_index_panel) + self.dlg.check_results.sort() + + if with_gui and self.dlg.check_results.has_rows(): + self.dlg.mOptionsListWidget.setCurrentRow(self.dlg.mOptionsListWidget.count() - 2) + self.dlg.tab_log.setCurrentIndex(0) self.dlg.out_log.moveCursor(QTextCursor.Start) self.dlg.out_log.ensureCursorVisible() - if Qgis.QGIS_VERSION_INT >= qgis_min_required_not_prod_ready: - if with_gui and error_cfg_saving and not ignore_error: - self.dlg.log_panel.append(tr('Issues which can be fixed automatically'), Html.H2) - self.dlg.log_panel.append(tr( - 'You have issue(s) listed above, and there is a wizard to auto fix your project. Saving the ' - 'configuration file is stopping.'), Html.Strong, time=True) - self.dlg.display_message_bar( - "Error", tr('You must fix some issues about this project'), Qgis.Critical) - return None + if self.dlg.check_results.has_blocking() and not ignore_error: + self.dlg.display_message_bar( + tr("Blocking issue"), + tr("The project has at least one blocking issue. The file is not saved."), + Qgis.Critical, + ) + return None metadata = { 'qgis_desktop_version': qgis_version(), @@ -3210,12 +3098,11 @@ def project_config_file( if valid is not None: metadata['project_valid'] = valid - if not valid: - warnings.append(Warnings.OgcNotValid.value) liz2json = dict() liz2json['metadata'] = metadata - liz2json['warnings'] = warnings + if self.dlg.include_in_cfg.isChecked(): + liz2json['warnings'] = self.dlg.check_results.to_json() liz2json["options"] = dict() liz2json["layers"] = dict() @@ -3554,26 +3441,14 @@ def check_project_validity(self): validator = QgsProjectServerValidator() valid, results = validator.validate(self.project) - if not valid: - self.dlg.log_panel.append(tr("OGC validation"), style=Html.H2) - self.dlg.log_panel.append( - tr("According to OGC standard : {}").format(tr('Valid') if valid else tr('Not valid')), Html.P) - self.dlg.log_panel.append( - tr( - "Open the 'Project properties', then 'QGIS Server' tab, at the bottom, you can check your project " - "according to OGC standard. If you need to fix a layer shortname, go to the 'Layer properties' " - "for the given layer, then 'QGIS Server' tab, edit the shortname." - ), Html.P) - LOGGER.info(f"Project has been detected : {'VALID' if valid else 'NOT valid'} according to OGC validation.") - if not valid: - message = tr( - 'The QGIS project is not valid according to OGC standards. You should check ' - 'messages in the "Project properties" → "QGIS Server" tab then "Test configuration" at the bottom. ' - '{} error(s) have been found').format(len(results)) - # noinspection PyUnresolvedReferences - self.iface.messageBar().pushMessage('Lizmap', message, level=Qgis.Warning, duration=DURATION_WARNING_BAR) + self.dlg.check_results.add_error( + Error( + Path(self.project.fileName()).name, + Checks.OgcValid, + ) + ) self.dlg.check_api_key_address() @@ -3682,6 +3557,16 @@ def save_cfg_file( Check the user defined data from GUI and save them to both global and project config files. """ + server_metadata = self.dlg.server_combo.currentData(ServerComboData.JsonMetadata.value) + self.dlg.check_results.truncate() + beginner_mode = QgsSettings().value(Settings.key(Settings.BeginnerMode), True, bool) + self.dlg.html_help.setHtml( + Checks.html( + severity=Severities.Blocking if beginner_mode else Severities.Important, + lizmap_cloud=is_lizmap_cloud(server_metadata) + ) + ) + self.dlg.log_panel.clear() self.dlg.log_panel.append(tr('Start saving the Lizmap configuration'), style=Html.P, time=True) variables = self.project.customVariables() @@ -3736,27 +3621,6 @@ def save_cfg_file( )) return False - duplicated_in_cfg = duplicated_layer_name_or_group(self.project) - - # message = tr('Some layer(s) or group(s) have a duplicated name in the legend.') - # message += '\n\n' - # message += tr( - # "It's not possible to store all the Lizmap configuration for these layer(s) or group(s), you should " - # "change them to make them unique and reconfigure their settings in the 'Layers' tab of the plugin.") - # message += '\n\n' - # display = False - for name, count in duplicated_in_cfg.items(): - if count >= 2: - from lizmap.models.check_project import Checks, Error - identifier = '"{}" → "'.format(name) + tr("count {} layers").format(count) + '\n' - self.dlg.check_results.add_error(Error(identifier, Checks.DuplicatedLayerNameOrGroup)) - # display = True - # message += '\n\n' - # message += stop_process - # if display: - # ScrollMessageBox(self.dlg, QMessageBox.Warning, tr('Configuration error'), message) - # return False - if not self.is_dev_version: if not self.server_manager.check_lwc_version(lwc_version.value): QMessageBox.critical( diff --git a/lizmap/project_checker_tools.py b/lizmap/project_checker_tools.py index 3648ce83..a609d113 100644 --- a/lizmap/project_checker_tools.py +++ b/lizmap/project_checker_tools.py @@ -17,25 +17,20 @@ QgsWkbTypes, ) +from lizmap.definitions.lizmap_cloud import CLOUD_DOMAIN from lizmap.qgis_plugin_tools.tools.i18n import tr -from lizmap.saas import ( - SAAS_DOMAIN, - SAAS_NAME, - edit_connection, - edit_connection_title, - right_click_step, -) from lizmap.tools import is_vector_pg, update_uri +from lizmap.widgets.check_project import Checks, SourceLayer """ Some checks which can be done on a layer. """ # https://github.com/3liz/lizmap-web-client/issues/3692 -FORCE_LOCAL_FOLDER = tr('Prevent file based layers to be in a parent folder') +FORCE_LOCAL_FOLDER = tr('Prevent file based layers from being in a parent folder') ALLOW_PARENT_FOLDER = tr('Allow file based layers to be in a parent folder') -PREVENT_NETWORK_DRIVE = tr('Prevent file based layers to be stored on another network drive') -PREVENT_SERVICE = tr('Prevent PostgreSQL layers to use a service file') -PREVENT_AUTH_DB = tr('Prevent PostgreSQL layers to use the authentication database') +PREVENT_OTHER_DRIVE = tr('Prevent file based layers from being stored on another drive (network or local)') +PREVENT_SERVICE = tr('Prevent PostgreSQL layers from using a service file') +PREVENT_AUTH_DB = tr('Prevent PostgreSQL layers from using the QGIS authentication database') FORCE_PG_USER_PASS = tr( 'PostgreSQL layers, if using a user and password, must have credentials saved in the datasource') PREVENT_ECW = tr('Prevent from using a ECW raster') @@ -47,75 +42,40 @@ def project_safeguards_checks( prevent_auth_id: bool, prevent_service: bool, force_pg_user_pass: bool, - prevent_network_drive: bool, + prevent_other_drive: bool, allow_parent_folder: bool, parent_folder: str, lizmap_cloud: bool, -) -> Tuple[Dict[str, str], str]: +) -> Dict: """ Check the project about safeguards. """ # Do not use homePath, it's not designed for this if the user has set a custom home path project_home = Path(project.absolutePath()) - layer_error: Dict[str, str] = {} + results = {} - connection_error = False for layer in project.mapLayers().values(): if isinstance(layer, QgsRasterLayer): if layer.source().lower().endswith('ecw') and prevent_ecw: - if lizmap_cloud: - layer_error[layer.name()] = tr( - 'The layer "{}" is an ECW. Because of the ECW\'s licence, this format is not compatible with ' - 'QGIS server. You should switch to a COG format.' - ).format(layer.name()) - else: - layer_error[layer.name()] = tr( - 'The layer "{}" is an ECW. You have activated a safeguard about preventing you using an ECW ' - 'layer. Either switch to a COG format or disable this safeguard.' - ).format(layer.name()) + results[SourceLayer(layer.name(), layer.id())] = Checks.PreventEcw if is_vector_pg(layer): # Make a copy by using a string, so we are sure to have user or password datasource = QgsDataSourceUri(layer.source()) if datasource.authConfigId() != '' and prevent_auth_id: - if lizmap_cloud: - layer_error[layer.name()] = tr( - 'The layer "{}" is using the QGIS authentication database. You must either use a PostgreSQL ' - 'service or store the login and password in the layer.' - ).format(layer.name()) - else: - layer_error[layer.name()] = tr( - 'The layer "{}" is using the QGIS authentication database. You have activated a safeguard ' - 'preventing you using the QGIS authentication database. Either switch to another ' - 'authentication mechanism or disable this safeguard.' - ).format(layer.name()) - connection_error = True + results[SourceLayer(layer.name(), layer.id())] = Checks.AuthenticationDb # We can continue continue if datasource.service() != '' and prevent_service: - layer_error[layer.name()] = tr( - 'The layer "{}" is using the PostgreSQL service file. Using a service file can be recommended in ' - 'many cases, but it requires a configuration step. If you have done the configuration (on the ' - 'server side mainly), you can disable this safeguard.' - ).format(layer.name()) + results[SourceLayer(layer.name(), layer.id())] = Checks.PgService # We can continue continue - if datasource.host().endswith(SAAS_DOMAIN) or force_pg_user_pass: + if datasource.host().endswith(CLOUD_DOMAIN) or force_pg_user_pass: if not datasource.username() or not datasource.password(): - if lizmap_cloud: - layer_error[layer.name()] = tr( - 'The layer "{}" is missing some credentials. Either the user and/or the password is not in ' - 'the layer datasource.' - ).format(layer.name()) - else: - layer_error[layer.name()] = tr( - 'The layer "{}" is missing some credentials. Either the user and/or the password is not in ' - 'the layer datasource, or disable the safeguard.' - ).format(layer.name()) - connection_error = True + results[SourceLayer(layer.name(), layer.id())] = Checks.PgForceUserPass # We can continue continue @@ -138,47 +98,44 @@ def project_safeguards_checks( # https://docs.python.org/3/library/os.path.html#os.path.relpath # On Windows, ValueError is raised when path and start are on different drives. # For instance, H: and C: - # Lizmap Cloud message must be prioritized - if lizmap_cloud: - layer_error[layer.name()] = tr( - 'The layer "{}" can not be hosted on {} because the layer is hosted on a different drive.' - ).format(layer.name(), SAAS_NAME) - continue - elif prevent_network_drive: - layer_error[layer.name()] = tr( - 'The layer "{}" is on another drive. Either move this file based layer or disable this safeguard.' - ).format(layer.name()) + + if lizmap_cloud or prevent_other_drive: + results[SourceLayer(layer.name(), layer.id())] = Checks.PreventDrive continue # Not sure what to do for now... # We can't compute a relative path, but the user didn't enable the safety check, so we must still skip continue - if parent_folder in relative_path and allow_parent_folder: - if lizmap_cloud: - # The layer can only be hosted the in "/qgis" directory - layer_error[layer.name()] = tr( - 'The layer "{}" can not be hosted on {} because the layer is located in too many ' - 'parent\'s folder. The current path from the project home path to the given layer is "{}".' - ).format(layer.name(), SAAS_NAME, relative_path) - else: - layer_error[layer.name()] = tr( - 'The layer "{}" is located in too many parent\'s folder. Either move this file based layer or ' - 'disable this safeguard. The current path from the project home path to the given layer is "{}".' - ).format(layer.name(), relative_path) - - more = '' - if connection_error: - more = edit_connection_title + " " - more += edit_connection + " " - more += '
' - more += right_click_step + " " - more += tr( - "When opening a QGIS project in your desktop, you mustn't have any " - "prompt for a user&password." - ) + if allow_parent_folder: + # The user allow parent folder, so we check against the string provided in the function call + if parent_folder in relative_path: + results[SourceLayer(layer.name(), layer.id())] = Checks.PreventParentFolder + else: + # The user wants only local files, we only check for ".." + if '..' in relative_path: + results[SourceLayer(layer.name(), layer.id())] = Checks.PreventParentFolder + + return results + + +def project_invalid_pk(project: QgsProject) -> Tuple[List[SourceLayer], List[SourceLayer]]: + """ Check either non existing PK or bigint. """ + autogenerated_keys = [] + int8 = [] + for layer in project.mapLayers().values(): + + if not isinstance(layer, QgsVectorLayer): + continue + + result, field = auto_generated_primary_key_field(layer) + if result: + autogenerated_keys.append(SourceLayer(layer.name(), layer.id())) + + if invalid_int8_primary_key(layer): + int8.append(SourceLayer(layer.name(), layer.id())) - return layer_error, more + return autogenerated_keys, int8 def auto_generated_primary_key_field(layer: QgsVectorLayer) -> Tuple[bool, Optional[str]]: @@ -310,7 +267,7 @@ def duplicated_layer_with_filter(project: QgsProject) -> Optional[str]: return text -def simplify_provider_side(project: QgsProject, fix=False) -> List[str]: +def simplify_provider_side(project: QgsProject, fix=False) -> List[SourceLayer]: """ Return the list of layer name which can be simplified on the server side. """ results = [] for layer in project.mapLayers().values(): @@ -323,7 +280,7 @@ def simplify_provider_side(project: QgsProject, fix=False) -> List[str]: if not layer.simplifyMethod().forceLocalOptimization(): continue - results.append(layer.name()) + results.append(SourceLayer(layer.name(), layer.id())) if fix: simplify = layer.simplifyMethod() @@ -333,7 +290,7 @@ def simplify_provider_side(project: QgsProject, fix=False) -> List[str]: return results -def use_estimated_metadata(project: QgsProject, fix: bool = False) -> List[str]: +def use_estimated_metadata(project: QgsProject, fix: bool = False) -> List[SourceLayer]: """ Return the list of layer name which can use estimated metadata. """ results = [] for layer in project.mapLayers().values(): @@ -342,7 +299,7 @@ def use_estimated_metadata(project: QgsProject, fix: bool = False) -> List[str]: uri = layer.dataProvider().uri() if not uri.useEstimatedMetadata(): - results.append(layer.name()) + results.append(SourceLayer(layer.name(), layer.id())) if fix: uri.setUseEstimatedMetadata(True) diff --git a/lizmap/resources/css/log.css b/lizmap/resources/css/log.css index 7198f5a7..4acc9465 100644 --- a/lizmap/resources/css/log.css +++ b/lizmap/resources/css/log.css @@ -75,3 +75,9 @@ hr { font-weight: bold; padding-top:25px; } + +table, th, td { + border: 1px solid black; + border-collapse: collapse; + padding:2px; +} diff --git a/lizmap/resources/ui/ui_lizmap.ui b/lizmap/resources/ui/ui_lizmap.ui index 13a53b50..e956eca1 100755 --- a/lizmap/resources/ui/ui_lizmap.ui +++ b/lizmap/resources/ui/ui_lizmap.ui @@ -4256,8 +4256,8 @@ This is different to the map maximum extent (defined in QGIS project properties, 0 0 - 450 - 499 + 601 + 488 @@ -4288,6 +4288,13 @@ This is different to the map maximum extent (defined in QGIS project properties, + + + + This feature is not linked to the QGIS native atlas feature. Both tools are independent. + + + @@ -4605,36 +4612,80 @@ This is different to the map maximum extent (defined in QGIS project properties, - 1 + 0 - + - Raw log + Checks - + - - + + + Checks which are failing. 'Blockings' are blocking :) To fix, either use the tooltip in the last column, or check the documentation in the next tab for all errors which can be reported + + true - + - Clear log + TEMPORARY, include in CFG ? + + + true + + + - + - Table log + Help about checks - + - + + + TEMPORARY, this doc is autogenerated, polishing + + + + + + + + + + + Raw logs + + + + + + Raw log, kind of deprecated later + + + + + + + true + + + + + + + Clear log + + @@ -4769,13 +4820,13 @@ This is different to the map maximum extent (defined in QGIS project properties, - Safe guards + Safeguards - Only if you are sure about your server and if you are more comfortable with Lizmap, you can disable some safe guards + Only if you are sure about your server and if you are more comfortable with Lizmap, you can disable some safeguards true @@ -4855,9 +4906,9 @@ This is different to the map maximum extent (defined in QGIS project properties, - + - SET PYTHON PREVENT ANOTHER NETWORK DRIVE + SET PYTHON PREVENT ANOTHER DRIVE false @@ -4938,23 +4989,23 @@ This is different to the map maximum extent (defined in QGIS project properties, QgsCollapsibleGroupBox QGroupBox -
qgscollapsiblegroupbox.h
+
qgis.gui
1
QgsFieldComboBox QComboBox -
qgsfieldcombobox.h
+
qgis.gui
QgsMapLayerComboBox QComboBox -
qgsmaplayercombobox.h
+
qgis.gui
QgsPasswordLineEdit QLineEdit -
qgspasswordlineedit.h
+
qgis.gui
1
@@ -4978,7 +5029,7 @@ This is different to the map maximum extent (defined in QGIS project properties, TableCheck QTableWidget -
lizmap.models.check_project
+
lizmap.widgets.check_project
diff --git a/lizmap/saas.py b/lizmap/saas.py index 3c656986..1963eaba 100644 --- a/lizmap/saas.py +++ b/lizmap/saas.py @@ -8,8 +8,10 @@ from qgis.core import QgsDataSourceUri, QgsProject +from lizmap.definitions.lizmap_cloud import CLOUD_DOMAIN from lizmap.qgis_plugin_tools.tools.i18n import tr from lizmap.tools import is_vector_pg, update_uri +from lizmap.widgets.check_project import SourceLayer edit_connection_title = tr("You must edit the database connection.") edit_connection = tr( @@ -22,10 +24,6 @@ "legend and click 'Change datasource' to pick the layer again with the updated connection." ) -SAAS_DOMAIN = 'lizmap.com' -SAAS_NAME = 'Lizmap Cloud' -SAAS_MAX_PARENT_FOLDER = 2 # TODO Check COG, is-it 3 ? - def is_lizmap_cloud(metadata: dict) -> bool: """ Return True if the metadata is coming from Lizmap Cloud. """ @@ -33,12 +31,12 @@ def is_lizmap_cloud(metadata: dict) -> bool: # Mainly in tests? return False - return metadata.get('hosting', '') == SAAS_DOMAIN + return metadata.get('hosting', '') == CLOUD_DOMAIN -def check_project_ssl_postgis(project: QgsProject) -> Tuple[List[str], str]: +def check_project_ssl_postgis(project: QgsProject) -> Tuple[List[SourceLayer], str]: """ Check if the project is not using SSL on some PostGIS layers which are on a Lizmap Cloud database. """ - layer_error: List[str] = [] + layer_error: List[SourceLayer] = [] for layer in project.mapLayers().values(): if not is_vector_pg(layer): continue @@ -52,11 +50,11 @@ def check_project_ssl_postgis(project: QgsProject) -> Tuple[List[str], str]: continue # Users might be hosted on Lizmap Cloud but using an external database - if not datasource.host().endswith(SAAS_DOMAIN): + if not datasource.host().endswith(CLOUD_DOMAIN): continue if datasource.sslMode() in (QgsDataSourceUri.SslMode.SslDisable, QgsDataSourceUri.SslMode.SslAllow): - layer_error.append(layer.name()) + layer_error.append(SourceLayer(layer.name(), layer.id())) more = edit_connection_title + " " more += edit_connection + " " @@ -77,7 +75,7 @@ def fix_ssl(project: QgsProject, force: bool = True) -> int: if datasource.service(): continue - if not datasource.host().endswith(SAAS_DOMAIN): + if not datasource.host().endswith(CLOUD_DOMAIN): continue if datasource.sslMode() in (QgsDataSourceUri.SslPrefer, QgsDataSourceUri.SslMode.SslRequire): diff --git a/lizmap/test/manuel_table_checks.py b/lizmap/test/manuel_table_checks.py deleted file mode 100644 index bcb8e6aa..00000000 --- a/lizmap/test/manuel_table_checks.py +++ /dev/null @@ -1,16 +0,0 @@ -from qgis.PyQt.QtWidgets import QApplication - -from lizmap.models.check_project import Checks, Error, TableCheck - -# from lizmap.widgets.check_project import CheckProjectView - - -app = QApplication([]) -view = TableCheck() -# model = TableModel() -# view = CheckProjectView() -# view.setModel(model) -identifier = '"bob" → count 2 layers' -view.add_error(Error(identifier, Checks.DuplicatedLayerNameOrGroup)) -view.show() -app.exec_() diff --git a/lizmap/test/test_table_checks.py b/lizmap/test/test_table_checks.py index 9e467943..6c34c449 100644 --- a/lizmap/test/test_table_checks.py +++ b/lizmap/test/test_table_checks.py @@ -1,30 +1,35 @@ import unittest -from qgis.PyQt.QtWidgets import QApplication - -from lizmap.models.check_project import Levels, Severities, TableCheck -from lizmap.widgets.check_project import CheckProjectView +from lizmap.widgets.check_project import Checks, Error, TableCheck class TestProjectTable(unittest.TestCase): - def setUp(self): - self.app = QApplication([]) - def test(self): - table = TableCheck(self.app.parent()) + table = TableCheck(None) + table.setup() self.assertEqual(table.horizontalHeader().count(), 4) self.assertEqual(table.verticalHeader().count(), 0) self.assertEqual(table.rowCount(), 0) - table.add_row(Severities.Blocking, Levels.Project, "Classical mechanics", "bob") + table.add_error(Error('home-sweet-home', Checks.DuplicatedLayerNameOrGroup)) self.assertEqual(table.rowCount(), 1) - -if __name__ == '__main__': - app = QApplication([]) - view = CheckProjectView() - view.show() - app.exec_() + expected = [ + { + 'error': 'duplicated_layer_name_or_group', + 'level': 'project', + 'severity': 1, + 'source': 'home-sweet-home' + } + ] + self.assertListEqual(expected, table.to_json()) + + +# if __name__ == '__main__': +# app = QApplication([]) +# # view = CheckProjectView() +# # view.show() +# app.exec_() diff --git a/lizmap/test/test_ui.py b/lizmap/test/test_ui.py index fd1a77b8..fa3d0fbc 100755 --- a/lizmap/test/test_ui.py +++ b/lizmap/test/test_ui.py @@ -82,7 +82,7 @@ def test_legend_options(self): self.assertIsNone(output['layers']['legend_displayed_startup'].get('noLegendImage')) # For LWC 3.5 - output = lizmap.project_config_file(LwcVersions.Lizmap_3_5, with_gui=False, check_server=False) + output = lizmap.project_config_file(LwcVersions.Lizmap_3_5, with_gui=False, check_server=False, ignore_error=True) self.assertIsNone(output['layers']['legend_displayed_startup'].get('legend_image_option')) self.assertEqual(output['layers']['legend_displayed_startup']['noLegendImage'], str(False)) diff --git a/lizmap/widgets/check_project.py b/lizmap/widgets/check_project.py index f2facc06..66619174 100644 --- a/lizmap/widgets/check_project.py +++ b/lizmap/widgets/check_project.py @@ -2,15 +2,728 @@ __license__ = 'GPL version 3' __email__ = 'info@3liz.org' -from qgis.PyQt.QtWidgets import QAbstractItemView, QTableView +from enum import Enum +from qgis.core import ( + QgsMapLayerModel, + QgsMarkerSymbol, + QgsProject, + QgsSymbolLayerUtils, +) +from qgis.PyQt.QtCore import QSize, Qt +from qgis.PyQt.QtGui import QIcon +from qgis.PyQt.QtWidgets import ( + QAbstractItemView, + QTableWidget, + QTableWidgetItem, +) -class CheckProjectView(QTableView): +from lizmap.definitions.lizmap_cloud import CLOUD_MAX_PARENT_FOLDER, CLOUD_NAME +from lizmap.definitions.qgis_settings import Settings +from lizmap.qgis_plugin_tools.tools.i18n import tr +from lizmap.tools import qgis_version + + +class Header: + + """ Header in tables. """ + def __init__(self, data: str, label: str, tooltip: str): + self.data = data + self.label = label + self.tooltip = tooltip + + +class Headers(Header, Enum): + """ List of headers in the table. """ + Severity = 'severity', tr('Severity'), tr("Severity of the error") + Level = 'level', tr('Level'), tr("Level of the error") + Object = 'source', tr('Source'), tr("Source of the error") + Error = 'error', tr('Error'), tr('Description of the error') + + +class Severity: + + """ A level of severity, if it's blocking or not. """ + def __init__(self, data: int, label: str, tooltip: str, color, size: int): + self.data = data + self.label = label + self.color = color + self.size = size + self.tooltip = tooltip + + def marker(self) -> QIcon: + """ Marker used in the table. """ + pixmap = QgsSymbolLayerUtils.symbolPreviewPixmap( + QgsMarkerSymbol.createSimple( + { + "name": "circle", + "color": self.color, + "size": "{}".format(self.size), + } + ), + QSize(16, 16) + ) + return QIcon(pixmap) + + def __str__(self): + return f'Severity {self.data} : {self.label}' + + +class Severities(Severity, Enum): + """ List of severities. """ + Blocking = 0, tr('Blocking'), tr('This is blocking the CFG file'), 'red', 3 + Important = 1, tr('Important'), tr('This is important to fix, to improve performance'), 'orange', 2.5 + # Normal = 2, tr('Normal'), tr('This would be nice to have look'), 'blue', 2 + Low = 3, tr('Low'), tr('Nice to do'), 'yellow', 2 + # Some severities can only done on runtime, QGIS version and/or Lizmap Cloud + Unknown = 99, 'Unknown', 'Severity will be determined on runtime', 'green', 1 + + +class Level: + + """ Level which is raising the issue. Important to set the icon if possible. """ + def __init__(self, data: str, label: str, tooltip: str, icon: QIcon): + self.data = data + self.label = label + self.icon = icon + self.tooltip = tooltip + + def __str__(self): + return f'{self.data} : {self.label}' + + +class Levels: + + """ List of levels used. """ + + GlobalConfig = Level( + 'global', + tr('Global'), + tr('Issue in the global configuration, in QGIS or Lizmap settings'), + QIcon(':/images/themes/default/console/iconSettingsConsole.svg'), + ) + Project = Level( + 'project', + tr('Project'), + tr('Issue at the project level'), + QIcon(':/images/themes/default/mIconQgsProjectFile.svg'), + ) + Layer = Level( + 'layer', + tr('Layer'), + tr('Issue at the layer level'), + QIcon(':/images/themes/default/algorithms/mAlgorithmMergeLayers.svg'), + ) + + +class Check: + + """ Definition of a check. """ + def __init__( + self, + data: str, + title: str, + description: str, + helper: str, + level: Level, + severity: Severity, + icon: QIcon, + alt_description_lizmap_cloud: str = None, + alt_help_lizmap_cloud: str = None, + ): + self.data = data + self.title = title + self.description = description + self.alt_description = alt_description_lizmap_cloud + self.helper = helper + self.alt_help = alt_help_lizmap_cloud + self.level = level + self.severity = severity + self.icon = icon + + def description_text(self, lizmap_cloud: bool) -> str: + """ Return the best description of the check, depending on Lizmap Cloud. """ + if lizmap_cloud and self.alt_description: + return self.alt_description + else: + return self.description + + def help_text(self, lizmap_cloud: bool) -> str: + """ Return the best help of the check, depending on Lizmap Cloud. """ + if lizmap_cloud and self.alt_help: + return self.alt_help + else: + return self.helper + + def html_help(self, index: int, severity: Severity, lizmap_cloud: False) -> str: + """ HTML string to show in an HTML table. """ + row_class = '' + if index % 2: + row_class = "class=\"odd-row\"" + + html_str = ( + "" + "{title}" + "{description}" + "{how_to_fix}" + "{level}" + "{severity}" + "" + ).format( + row_class=row_class, + title=self.title, + description=self.description_text(lizmap_cloud), + how_to_fix=self.help_text(lizmap_cloud), + level=self.level.label, + severity=severity.label if self.severity == Severities.Unknown else self.severity.label, + ) + return html_str + + def html_tooltip(self, lizmap_cloud: bool = False) -> str: + """ HTML string to be used as a tooltip. """ + html_str = ( + "{description}" + "
" + "

{how_to_fix}

" + ).format( + description=self.description_text(lizmap_cloud), + how_to_fix=self.help_text(lizmap_cloud), + ) + return html_str + + def __str__(self): + return f'{self.title} : {self.description_text(False)} :{self.level} → {self.severity}' + + +# Check QGIS_VERSION_INT +qgis_32200 = tr( + 'With QGIS ≥ 3.22, you can use the auto-fix button in the "Settings" panel of the plugin to fix currently loaded ' + 'layers' +) +other_auth = tr('Either switch to another authentication mechanism') +safeguard = tr('Or disable this safeguard in your Lizmap plugin settings') +global_connection = tr( + 'To fix layers loaded later, edit your global PostgreSQL connection to enable this option, then change the ' + 'datasource by right clicking on each layer above, then click "Change datasource" in the menu. Finally reselect ' + 'your layer in the new dialog with the updated connection. When opening a QGIS project in your desktop, ' + 'you mustn\'t have any prompt for a user or password. ' + 'The edited connection will take effect only on newly added layer into a project that\'s why the right-click step ' + 'is required.' +) + + +class Checks(Check, Enum): + + """ List of checks defined. """ + + OgcValid = ( + 'ogc_validity', + tr('OGC validity'), + tr( + "According to OGC rules, the project is not valid." + ), + ( + '
    ' + '
  • {project_properties}
  • ' + '
  • {project_shortname}
  • ' + '
  • {layer_shortname}
  • ' + '
' + ).format( + project_properties=tr( + "Open the 'Project properties', then 'QGIS Server' tab, at the bottom, you can check your project " + "according to OGC standard" + ), + layer_shortname=tr( + "If you need to fix a layer shortname, go to the 'Layer properties' " + "for the given layer, then 'QGIS Server' tab, edit the shortname." + ), + project_shortname=tr( + "If you need to fix the project shortname, go to the 'Project properties', " + "then 'QGIS Server' tab, first tab, and change the shortname." + ), + ), + Levels.Project, + Severities.Low, + QIcon(':/images/themes/default/mIconWms.svg'), + ) + PkInt8 = ( + 'primary_key_bigint', + tr('Invalid bigint (integer8) field for QGIS Server as primary key'), + tr( + "Primary key should be an integer. If not fixed, expect layer to have some issues with some tools in " + "Lizmap Web Client: zoom to feature, filtering…" + ), + ( + '
    ' + '
  • {help}
  • ' + '
' + ).format( + help=tr( + "We highly recommend you to set a proper integer field as a primary key, but neither a bigint nor " + "an integer8." + ), + ), + Levels.Layer, + Severities.Important, + QIcon(':/images/themes/default/mIconFieldInteger.svg'), + ) + MissingPk = ( + 'missing_primary_key', + tr('Missing a proper primary key in the database.'), + tr( + "The layer must have a proper primary key defined. When it's missing, QGIS Desktop tried to set a " + "temporary field called 'tid/ctid/…' to be a unique identifier. On QGIS Server, this will bring issues." + ), + ( + '
    ' + '
  • {help}
  • ' + '
' + ).format( + help=tr( + "We highly recommend you to set a proper integer field as a primary key, but neither a bigint nor " + "an integer8." + ), + ), + Levels.Layer, + Severities.Important, + QIcon(':/images/themes/default/mSourceFields.svg'), + ) + SSLConnection = ( + 'ssl_connection', + tr('SSL connections to a PostgreSQL database'), + tr("Connections to a PostgreSQL database hosted on {} must use a SSL secured connection.").format(CLOUD_NAME), + ( + '
    ' + '
  • {auto_fix}
  • ' + '
  • {help}
  • ' + '
' + ).format( + auto_fix=qgis_32200, + help=global_connection, + ), + Levels.Layer, + Severities.Blocking if qgis_version() >= 32200 else Severities.Important, + QIcon(':/images/themes/default/mIconPostgis.svg'), + ) + EstimatedMetadata = ( + 'estimated_metadata', + tr('Estimated metadata'), + tr("PostgreSQL layer can have the use estimated metadata option enabled"), + ( + '
    ' + '
  • {auto_fix}
  • ' + '
  • {help}
  • ' + '
' + ).format( + auto_fix=qgis_32200, + help=global_connection, + ), + Levels.Layer, + Severities.Blocking if qgis_version() >= 32200 else Severities.Important, + QIcon(':/images/themes/default/mIconPostgis.svg'), + ) + SimplifyGeometry = ( + 'simplify_geometry', + tr('Simplify geometry on the provider side'), + tr("PostgreSQL layer can have the simplification on the provider side enabled"), + ( + '
    ' + '
  • {auto_fix}
  • ' + '
  • {help}
  • ' + '
' + ).format( + auto_fix=qgis_32200, + help=tr( + 'Visit the layer properties, then in the "Rendering" tab to enable it simplification on the provider ' + 'side on the given layer.' + ), + ), + Levels.Layer, + Severities.Blocking if qgis_version() >= 32200 else Severities.Important, + QIcon(':/images/themes/default/mIconGeometryCollectionLayer.svg'), + ) + DuplicatedLayerNameOrGroup = ( + 'duplicated_layer_name_or_group', + tr('Duplicated layer name or group'), + tr("It's not possible to store all the Lizmap configuration for these layer(s) or group(s)."), + ( + '
    ' + '
  • {}
  • ' + '
  • {}
  • ' + '
' + ).format( + tr('You must change them to make them unique'), + tr('Reconfigure their settings in the "Layers" tab of the plugin') + ), + Levels.Project, + Severities.Important, + QIcon(':/images/themes/default/propertyicons/editmetadata.svg'), + ) + WmsUseLayerIds = ( + 'wms_use_layer_id', + tr('Do not use layer IDs as name'), + tr( + "It's not possible anymore to use the option 'Use layer IDs as name' in the project properties dialog, " + "QGIS server tab, then WMS capabilities." + ), + '
  • {help}
'.format( + help=tr("Uncheck this checkbox and re-save the Lizmap configuration file") + ), + Levels.Project, + Severities.Blocking, + QIcon(':/images/themes/default/mIconWms.svg'), + ) + TrustProject = ( + 'trust_project_metadata', + tr('Trust project metadata'), + tr('The project does not have the "Trust project metadata" enabled at the project level'), + ( + '
    ' + '
  • {auto_fix}
  • ' + '
  • {help}
  • ' + '
'.format( + help=tr( + 'In the project properties → Data sources → at the bottom, there is a checkbox to trust the ' + 'project when the layer has no metadata.' + ), + auto_fix=qgis_32200, + ) + ), + Levels.Project, + Severities.Blocking if qgis_version() >= 32200 else Severities.Important, + QIcon(':/images/themes/default/mIconQgsProjectFile.svg'), + ) + PreventEcw = ( + Settings.PreventEcw, + tr('Prevent ECW raster'), + tr( + 'The layer is using the ECW raster format. Because of the ECW\'s licence, this format is not compatible ' + 'with most of QGIS server installations. You have activated a safeguard about preventing you using an ' + 'ECW layer.'), + ( + '
    ' + '
  • {help}
  • ' + '
  • {other}
  • ' + '
'.format( + help=tr('Either switch to a COG format'), + other=safeguard, + ) + ), + Levels.Layer, + Severities.Unknown, + QIcon(':/images/themes/default/mIconRasterLayer.svg'), + tr( + 'The layer is using an ECW raster format. Because of the ECW\'s licence, this format is not compatible ' + 'with QGIS server.' + ), + ( + '
    ' + '
  • {help}
  • ' + '
' + ).format(help=tr('Switch to a COG format')) + ) + AuthenticationDb = ( + Settings.PreventPgAuthDb, + tr('Authentication database'), + tr( + 'The layer is using the QGIS authentication database. You have activated a safeguard preventing you using ' + 'the QGIS authentication database.' + ), + ( + '
    ' + '
  • {help}
  • ' + '
  • {other}
  • ' + '
  • {global_connection}
  • ' + '
'.format( + help=other_auth, + other=safeguard, + global_connection=global_connection, + ) + ), + Levels.Layer, + Severities.Unknown, + QIcon(':/images/themes/default/mIconPostgis.svg'), + tr('The layer is using the QGIS authentication database. This is not compatible with {}').format(CLOUD_NAME), + ( + '
    ' + '
  • {service}
  • ' + '
  • {login_pass}
  • ' + '
' + ).format( + service=tr('Either use a PostgreSQL service'), + login_pass=tr('Or store the login and password in the layer.') + ) + ) + PgService = ( + Settings.PreventPgService, + tr('PostgreSQL service'), + tr( + 'Using a PostgreSQL service file can be recommended in many cases, but it requires a configuration step. ' + 'If you have done the configuration (on the server side mainly), you can disable this safeguard.' + ), + ( + '
    ' + '
  • {help}
  • ' + '
  • {other}
  • ' + '
  • {global_connection}
  • ' + '
'.format( + help=other_auth, + other=safeguard, + global_connection=global_connection, + ) + ), + Levels.Layer, + Severities.Unknown, + QIcon(':/images/themes/default/mIconPostgis.svg'), + ) + PgForceUserPass = ( + Settings.ForcePgUserPass, + tr('PostgreSQL user and/or password'), + tr( + 'The layer is missing some credentials, either user and/or password.' + ), + ( + '
    ' + '
  • {edit_layer}
  • ' + '
  • {help}
  • ' + '
  • {other}
  • ' + '
  • {global_connection}
  • ' + '
'.format( + edit_layer=tr('Edit your layer configuration by force saving user&password'), + help=other_auth, + other=safeguard, + global_connection=global_connection, + ) + ), + Levels.Layer, + Severities.Unknown, + QIcon(':/images/themes/default/mIconPostgis.svg'), + ) + PreventDrive = ( + Settings.PreventDrive, + tr('Other drive (network or local)'), + tr('The layer is stored on another drive.'), + ( + '
    ' + '
  • {help}
  • ' + '
  • {other}
  • ' + '
'.format( + help=tr('Either move this file based layer'), + other=safeguard, + ) + ), + Levels.Layer, + Severities.Unknown, + QIcon(':/qt-project.org/styles/commonstyle/images/networkdrive-16.png'), + tr('The layer is stored on another drive, which is not possible using {}.').format(CLOUD_NAME), + ( + '
    ' + '
  • {help}
  • ' + '
' + ).format( + help=tr('Move the layer'), + ) + ) + PreventParentFolder = ( + Settings.AllowParentFolder, + tr('Parent folder'), + tr('The layer is stored in too many parent\'s folder, compare to the QGS file.'), + ( + '
    ' + '
  • {help}
  • ' + '
  • {other}
  • ' + '
'.format( + help=tr('Either move this file based layer'), + other=safeguard, + ) + ), + Levels.Layer, + Severities.Unknown, + QIcon(':/images/themes/default/mIconFolderOpen.svg'), + tr('The layer is stored in too many parent\'s folder, compare to the QGS file.'), + ( + '
    ' + '
  • {help}
  • ' + '
  • {other}
  • ' + '
  • {fyi}
  • ' + '
' + ).format( + help=tr('Either move the layer'), + other=safeguard, + fyi=tr( + 'For your information, the maximum of parents is {count} on {hosting_name}. This will be overriden ' + 'on runtime if you use a higher value according to the server selected in the first panel.' + ).format( + count=CLOUD_MAX_PARENT_FOLDER, + hosting_name=CLOUD_NAME + ), + ) + ) + + @classmethod + def html(cls, severity: Severity, lizmap_cloud: bool) -> str: + """ Generate an HTML table, according to the instance. """ + html_str = '' + html_str += ( + '' + ).format( + title=tr('Title'), + description=tr('Description'), + howto=tr('How to fix'), + level=tr('Level'), + severity=tr('Severity'), + ) + copy_sort = list(cls.__members__.values()) + copy_sort.sort(key=lambda x: severity.data if x.severity == Severities.Unknown else x.severity.data) + for i, check in enumerate(copy_sort): + html_str += check.html_help(i, severity, lizmap_cloud) + html_str += '
{title}{description}{howto}{level}{severity}
' + return html_str + + +class SourceLayer: + + """ For identifying a layer in a project. """ + def __init__(self, layer_name, layer_id): + self.layer_id = layer_id + self.layer_name = layer_name + + +class SourceType: + + """ List of sources in the project. """ + + Layer = SourceLayer + + +class Error: + + """ An error is defined by a check and a source. """ + def __init__(self, source: str, check: Check, source_type=None): + self.source = source + self.check = check + self.source_type = source_type + + def __str__(self): + return f'{self.source} : {self.check}' + + +class TableCheck(QTableWidget): + + """ Subclassing of QTableWidget in the plugin. """ + + # noinspection PyUnresolvedReferences + DATA = Qt.UserRole + JSON = DATA + 1 + + def setup(self): + """ Setting up parameters. """ + # Do not use the constructor __init__, it's not working. Maybe because of UI files ? - def __init__(self, parent=None): - QTableView.__init__(self, parent=parent) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setEditTriggers(QAbstractItemView.NoEditTriggers) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setAlternatingRowColors(True) self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().setVisible(True) + # Bug, same as self.sort() + # self.setSortingEnabled(True) + + self.setColumnCount(len(Headers)) + for i, header in enumerate(Headers): + column = QTableWidgetItem(header.label) + column.setToolTip(header.tooltip) + self.setHorizontalHeaderItem(i, column) + + def truncate(self): + """ Truncate the table. """ + self.setRowCount(0) + + def has_blocking(self) -> bool: + """ If the table has at least one blocking issue. """ + for row in range(self.rowCount()): + if self.item(row, 0).data(self.DATA) == 0: + return True + return False + + def has_rows(self) -> int: + """ If the table has at least one row displayed. """ + return self.rowCount() + + def sort(self): + """ Sort the table by severity. """ + # Strange bug occurring when we launch the analysis on the second time + # Lines are disappearing + # self.sortByColumn(0, Qt.AscendingOrder) + pass + + def to_json(self) -> list: + """ Export data to JSON. """ + result = [] + + for row in range(self.rowCount()): + data = dict() + for i, header in enumerate(Headers): + data[header.data] = self.item(row, i).data(self.JSON) + result.append(data) + + return result + + def add_error(self, error: Error, lizmap_cloud: bool = False, severity=None): + """ Add an error in the table. """ + # By default, let's take the one in the error + used_severity = error.check.severity + if used_severity == Severities.Unknown: + if severity: + # The given severity is overriden the one in the error + used_severity = severity + else: + raise NotImplementedError('Missing severity level') + + row = self.rowCount() + self.setRowCount(row + 1) + + column = 0 + + # Severity + item = QTableWidgetItem(used_severity.label) + item.setData(self.DATA, used_severity.data) + item.setData(self.JSON, used_severity.data) + item.setToolTip(used_severity.tooltip) + item.setIcon(used_severity.marker()) + self.setItem(row, column, item) + column += 1 + + # Level + item = QTableWidgetItem(error.check.level.label) + item.setData(self.DATA, error.check.level.data) + item.setData(self.JSON, error.check.level.data) + item.setToolTip(error.check.level.tooltip) + item.setIcon(error.check.level.icon) + self.setItem(row, column, item) + column += 1 + + # Source + item = QTableWidgetItem(error.source) + item.setData(self.DATA, error.source) + if isinstance(error.source_type, SourceType.Layer): + item.setToolTip(error.source_type.layer_id) + layer = QgsProject.instance().mapLayer(error.source_type.layer_id) + item.setIcon(QgsMapLayerModel.iconForLayer(layer)) + item.setData(self.JSON, error.source_type.layer_id) + else: + # Project only for now + # TODO fix else + item.setData(self.JSON, error.source) + self.setItem(row, column, item) + column += 1 + + # Error + item = QTableWidgetItem(error.check.title) + item.setData(self.DATA, error.source) + item.setData(self.JSON, error.check.data) + item.setToolTip(error.check.html_tooltip(lizmap_cloud)) + if error.check.icon: + item.setIcon(error.check.icon) + self.setItem(row, column, item) + column += 1