From 132057f0e96ee24b0d44736d7dfc742cab23cb15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Kr=C3=B6ger?= Date: Sat, 2 Nov 2024 15:27:38 +0100 Subject: [PATCH 1/6] Big refactoring number 2 - Clean separation between GUI, core functions and manager in-between - Data source discovery rules in dataclasses with proper regexes - Transfer of data sources via stored data - Remove unnecessary path twiddling based on OS - Use pathlib everywhere possible - Less classes more functions #42 - Lots of renamings for clarity - Qt6 compatible enums - ... --- README.md | 5 +- profile_manager/datasources/bookmarks.py | 74 ++ .../datasources/bookmarks/__init__.py | 0 .../datasources/bookmarks/bookmark_handler.py | 89 --- ...tomization_handler.py => customization.py} | 27 +- .../datasources/customizations/__init__.py | 0 profile_manager/datasources/data_sources.py | 272 +++++++ .../datasources/dataservices/__init__.py | 0 .../dataservices/datasource_distributor.py | 255 ------- .../dataservices/datasource_handler.py | 171 ----- .../dataservices/datasource_provider.py | 212 ------ profile_manager/datasources/expressions.py | 40 + profile_manager/datasources/favourites.py | 49 ++ .../datasources/favourites/__init__.py | 0 .../favourites/favourites_handler.py | 58 -- .../datasources/functions/__init__.py | 0 .../datasources/functions/function_handler.py | 57 -- profile_manager/datasources/models.py | 34 + .../datasources/models/__init__.py | 0 .../datasources/models/model_handler.py | 34 - .../datasources/models/script_handler.py | 33 - profile_manager/datasources/plugins.py | 150 ++++ .../datasources/plugins/__init__.py | 0 .../datasources/plugins/plugin_displayer.py | 71 -- .../datasources/plugins/plugin_handler.py | 65 -- .../datasources/plugins/plugin_importer.py | 63 -- .../datasources/plugins/plugin_remover.py | 50 -- profile_manager/datasources/scripts.py | 33 + profile_manager/datasources/styles.py | 50 ++ .../datasources/styles/__init__.py | 0 .../datasources/styles/style_handler.py | 58 -- profile_manager/gui/interface_handler.py | 231 ------ profile_manager/gui/utils.py | 68 ++ profile_manager/profile_manager.py | 714 ++++++++---------- profile_manager/profile_manager_dialog.py | 596 ++++++++++++++- .../profile_manager_dialog_base.ui | 16 +- .../profiles/profile_action_handler.py | 48 -- profile_manager/profiles/profile_copier.py | 45 -- profile_manager/profiles/profile_creator.py | 58 -- profile_manager/profiles/profile_editor.py | 63 -- profile_manager/profiles/profile_handler.py | 85 +++ profile_manager/profiles/profile_remover.py | 74 -- profile_manager/utils.py | 18 - 43 files changed, 1772 insertions(+), 2194 deletions(-) create mode 100644 profile_manager/datasources/bookmarks.py delete mode 100644 profile_manager/datasources/bookmarks/__init__.py delete mode 100644 profile_manager/datasources/bookmarks/bookmark_handler.py rename profile_manager/datasources/{customizations/customization_handler.py => customization.py} (69%) delete mode 100644 profile_manager/datasources/customizations/__init__.py create mode 100644 profile_manager/datasources/data_sources.py delete mode 100644 profile_manager/datasources/dataservices/__init__.py delete mode 100644 profile_manager/datasources/dataservices/datasource_distributor.py delete mode 100644 profile_manager/datasources/dataservices/datasource_handler.py delete mode 100644 profile_manager/datasources/dataservices/datasource_provider.py create mode 100644 profile_manager/datasources/expressions.py create mode 100644 profile_manager/datasources/favourites.py delete mode 100644 profile_manager/datasources/favourites/__init__.py delete mode 100644 profile_manager/datasources/favourites/favourites_handler.py delete mode 100644 profile_manager/datasources/functions/__init__.py delete mode 100644 profile_manager/datasources/functions/function_handler.py create mode 100644 profile_manager/datasources/models.py delete mode 100644 profile_manager/datasources/models/__init__.py delete mode 100644 profile_manager/datasources/models/model_handler.py delete mode 100644 profile_manager/datasources/models/script_handler.py create mode 100644 profile_manager/datasources/plugins.py delete mode 100644 profile_manager/datasources/plugins/__init__.py delete mode 100644 profile_manager/datasources/plugins/plugin_displayer.py delete mode 100644 profile_manager/datasources/plugins/plugin_handler.py delete mode 100644 profile_manager/datasources/plugins/plugin_importer.py delete mode 100644 profile_manager/datasources/plugins/plugin_remover.py create mode 100644 profile_manager/datasources/scripts.py create mode 100644 profile_manager/datasources/styles.py delete mode 100644 profile_manager/datasources/styles/__init__.py delete mode 100644 profile_manager/datasources/styles/style_handler.py delete mode 100644 profile_manager/gui/interface_handler.py create mode 100644 profile_manager/gui/utils.py delete mode 100644 profile_manager/profiles/profile_action_handler.py delete mode 100644 profile_manager/profiles/profile_copier.py delete mode 100644 profile_manager/profiles/profile_creator.py delete mode 100644 profile_manager/profiles/profile_editor.py create mode 100644 profile_manager/profiles/profile_handler.py delete mode 100644 profile_manager/profiles/profile_remover.py diff --git a/README.md b/README.md index 53f47c8..eac2590 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,9 @@ To install the plugin manually just copy the folder into your QGIS profile direc - Importing (spatial) bookmarks - Importing (data source) favourites - Importing plugins -- Importing expression functions -- Importing models & scripts +- Importing expressions +- Importing models +- Importing scripts - Importing some symbology types & label settings - Importing QGIS UI settings (e.g. hidden toolbar items) diff --git a/profile_manager/datasources/bookmarks.py b/profile_manager/datasources/bookmarks.py new file mode 100644 index 0000000..26f0de1 --- /dev/null +++ b/profile_manager/datasources/bookmarks.py @@ -0,0 +1,74 @@ +from pathlib import Path + +from lxml import etree as et + + +def import_bookmarks(source_bookmark_file: Path, target_bookmark_file: Path): + """Imports spatial bookmarks from source to target file. + + Spatial bookmarks are stored in bookmarks.xml, e.g.: + + + + ... + + + ... + + + TODO The deduplication seems garbage + + Args: + source_bookmark_file: Path of bookmarks file to import from + target_bookmark_file: Path of bookmarks file to import to + """ + # get the element tree of the source file + source_tree = et.parse(source_bookmark_file, et.XMLParser(remove_blank_text=True)) + + # check if target file exists + if not target_bookmark_file.is_file(): + with open(target_bookmark_file, "w") as new_file: + new_file.write("") + + # get the element tree of the target file + # fill if empty + target_tree = et.parse(target_bookmark_file, et.XMLParser(remove_blank_text=True)) + + # find all bookmark elements + source_root_tag = source_tree.findall("Bookmark") + + # get the root element "Bookmarks" # TODO comment does not seem to fit the code? + target_tree_root = target_tree.getroot() + + # Remove duplicate entries to prevent piling data + target_tree_root = remove_duplicates(source_root_tag, target_tree, target_tree_root) + + # append the elements + for element in source_root_tag: + target_tree_root.append(element) + + # overwrite the xml file + et.ElementTree(target_tree_root).write( + target_bookmark_file, + pretty_print=True, + encoding="utf-8", + xml_declaration=True, + ) + + +def remove_duplicates( + source_root_tag: list[et.Element], + target_tree: et.ElementTree, + target_tree_root: et.Element, +): + """Removes bookmarks from target that exist in the (to be imported) source too.""" + # TODO FIXME this only checks the name of the bookmarks which will lead to false positives + # it is ok and supported by QGIS to have the same name for multiple bookmarks + # TODO compare the complete content of the xml node! + target_root_tag = target_tree.findall("Bookmark") + for source_element in source_root_tag: + for target_element in target_root_tag: + if source_element.attrib["name"] == target_element.attrib["name"]: + target_tree_root.remove(target_element) + + return target_tree_root diff --git a/profile_manager/datasources/bookmarks/__init__.py b/profile_manager/datasources/bookmarks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile_manager/datasources/bookmarks/bookmark_handler.py b/profile_manager/datasources/bookmarks/bookmark_handler.py deleted file mode 100644 index 7ffa314..0000000 --- a/profile_manager/datasources/bookmarks/bookmark_handler.py +++ /dev/null @@ -1,89 +0,0 @@ -from pathlib import Path - -from lxml import etree as et -from qgis.core import Qgis, QgsMessageLog - - -def import_bookmarks(source_bookmark_file: str, target_bookmark_file: str): - """Imports spatial bookmarks from source to target profile. - - Spatial bookmarks are stored in bookmarks.xml, e.g.: - - - - ... - - - ... - - - Args: - TODO - - Returns: - error_message (str): An error message, if something XML related failed. - """ - # get the element tree of the source file - QgsMessageLog.logMessage("import_bookmarks", "Profile Manager", level=Qgis.Critical) - try: - source_tree = et.parse( - source_bookmark_file, et.XMLParser(remove_blank_text=True) - ) - - # check if target file exists - create_bookmark_file_if_not_exist(target_bookmark_file) - # get the element tree of the target file - # fill if empty - target_tree = et.parse( - target_bookmark_file, et.XMLParser(remove_blank_text=True) - ) - - # find all bookmark elements - source_root_tag = source_tree.findall("Bookmark") - - # get the root element "Bookmarks" - target_tree_root = target_tree.getroot() - - # Remove duplicate entries to prevent piling data - target_tree_root = remove_duplicates( - source_root_tag, target_tree, target_tree_root - ) - - # append the elements - for element in source_root_tag: - target_tree_root.append(element) - - # overwrite the xml file - et.ElementTree(target_tree_root).write( - target_bookmark_file, - pretty_print=True, - encoding="utf-8", - xml_declaration=True, - ) - except et.Error as e: - # TODO: It would be nice to have a smaller and more specific try block but until then we except broadly - error = f"{type(e)}: {str(e)}" - QgsMessageLog.logMessage(error, "Profile Manager", level=Qgis.Warning) - return error - - -def remove_duplicates(source_root_tag, target_tree, target_tree_root): - """Removes bookmarks from target that exist in the (to be imported) source too.""" - # TODO FIXME this only checks the name of the bookmarks which will lead to false positives - # it is ok and supported by QGIS to have the same name for multiple bookmarks - # TODO compare the complete content of the xml node! - target_root_tag = target_tree.findall("Bookmark") - for s_element in source_root_tag: - for t_element in target_root_tag: - if s_element.attrib["name"] == t_element.attrib["name"]: - target_tree_root.remove(t_element) - - return target_tree_root - - -def create_bookmark_file_if_not_exist(bookmark_file): - """Checks if file exists and creates it if not""" - target_file = Path(bookmark_file) - if not target_file.is_file(): - with open(bookmark_file, "w") as new_file: - new_file.write("") diff --git a/profile_manager/datasources/customizations/customization_handler.py b/profile_manager/datasources/customization.py similarity index 69% rename from profile_manager/datasources/customizations/customization_handler.py rename to profile_manager/datasources/customization.py index 36b689b..766a0d1 100644 --- a/profile_manager/datasources/customizations/customization_handler.py +++ b/profile_manager/datasources/customization.py @@ -1,11 +1,9 @@ from configparser import RawConfigParser -from os import path +from pathlib import Path from shutil import copy2 -from profile_manager.utils import adjust_to_operating_system - -def import_customizations(source_profile_path: str, target_profile_path: str): +def import_customizations(source_profile_path: Path, target_profile_path: Path): """Imports UI customizations from source to target profile. Copies the whole QGISCUSTOMIZATION3.ini file and also transfers the [UI] section from QGIS3.ini if available @@ -19,25 +17,18 @@ def import_customizations(source_profile_path: str, target_profile_path: str): ... Args: - TODO + source_profile_path: Path of profile directory to import from + target_profile_path: Path of profile directory to import to """ # Copy (overwrite) the QGISCUSTOMIZATION3.ini if exist - source_customini_path = adjust_to_operating_system( - source_profile_path + "QGIS/QGISCUSTOMIZATION3.ini" - ) - target_customini_path = adjust_to_operating_system( - target_profile_path + "QGIS/QGISCUSTOMIZATION3.ini" - ) - if path.exists(source_customini_path): + source_customini_path = source_profile_path / "QGIS" / "QGISCUSTOMIZATION3.ini" + target_customini_path = target_profile_path / "QGIS" / "QGISCUSTOMIZATION3.ini" + if source_customini_path.exists(): copy2(source_customini_path, target_customini_path) # Copy [UI] section from QGIS3.ini - source_qgis3ini_path = adjust_to_operating_system( - source_profile_path + "QGIS/QGIS3.ini" - ) - target_qgis3ini_path = adjust_to_operating_system( - target_profile_path + "QGIS/QGIS3.ini" - ) + source_qgis3ini_path = source_profile_path / "QGIS" / "QGIS3.ini" + target_qgis3ini_path = target_profile_path / "QGIS" / "QGIS3.ini" source_ini_parser = RawConfigParser() source_ini_parser.optionxform = str # str = case-sensitive option names diff --git a/profile_manager/datasources/customizations/__init__.py b/profile_manager/datasources/customizations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile_manager/datasources/data_sources.py b/profile_manager/datasources/data_sources.py new file mode 100644 index 0000000..3d520d3 --- /dev/null +++ b/profile_manager/datasources/data_sources.py @@ -0,0 +1,272 @@ +import logging + +from configparser import RawConfigParser +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from re import compile +from urllib.parse import unquote + + +LOGGER = logging.getLogger("profile_manager") + + +@dataclass +class Rule: + """Section and regex for find data source connections in a QGIS3.ini file.""" + + section: str # INI section in which to search for regex matches + regex: str # regex to search for in the keys of the section + + +DATA_SOURCE_SEARCH_RULES = { + # Where to search for which provider's data sources in the QGIS3.ini. + # A list of searching rules each; not just one rule, because QGIS changed between + # versions AND stores stuff in various places anyways. + # This is based on a config grown over years, currently in 3.38. + # Each regex must include a group for getting the (encoded) *data source name*, + # e.g. ([^=^\\]+) between delimiters: + # - ^= to not get into the *value* part of the line into the group + # e.g. ows\items\WMS\connections\items\foo\http-header=@Variant(\0\0\0\b\0\0\0\0) + # - ^\\ to not get issues in the group if an option includes more backslashes + # e.g. connections-vector-tile\foo%20bar\http-header\referer=baz + "GeoPackage": [Rule("providers", r"^ogr\\GPKG\\connections\\([^=^\\]+)\\")], + "SpatiaLite": [Rule("SpatiaLite", r"^connections\\([^=^\\]+)\\")], + "PostgreSQL": [Rule("PostgreSQL", r"^connections\\([^=^\\]+)\\")], + "MS SQL Server": [Rule("MSSQL", r"^connections\\([^=^\\]+)\\")], + "Vector Tiles": [ + Rule("connections", r"^vector-tile\\items\\([^=^\\]+)\\"), + Rule("qgis", r"^connections-vector-tile\\([^=^\\]+)\\"), + ], + "WMS/WMTS": [ + # only authcfg, password and username at "ows\items\WMS", the rest is at "ows\items\wms" + Rule("connections", r"^ows\\items\\WMS\\connections\\items\\([^=^\\]+)\\"), + Rule("connections", r"^ows\\items\\wms\\connections\\items\\([^=^\\]+)\\"), + Rule("qgis", r"WMS\\([^=^\\]+)\\"), # only authcfg, password and username + # only authcfg, password and username at "connections\WMS", the rest is at "connections-wms" + Rule("qgis", r"^connections\\WMS\\([^=^\\]+)\\"), + Rule("qgis", r"^connections-wms\\([^=^\\]+)\\"), + ], + "WFS / OGC API - Features": [ # same as WMS + # only authcfg, password and username at "ows\items\WFS", the rest is at "ows\items\wfs" + Rule("connections", r"^ows\\items\\WFS\\connections\\items\\([^=^\\]+)\\"), + Rule("connections", r"^ows\\items\\wfs\\connections\\items\\([^=^\\]+)\\"), + Rule("qgis", r"WFS\\([^=^\\]+)\\"), # only authcfg, password and username + # only authcfg, password and username at "connections\WFS", the rest is at "connections-wfs" + Rule("qgis", r"^connections\\WFS\\([^=^\\]+)\\"), + Rule("qgis", r"^connections-wfs\\([^=^\\]+)\\"), + ], + "WCS": [ # same as WMS + # only authcfg, password and username at "ows\items\WCS", the rest is at "ows\items\wcs" + Rule("connections", r"^ows\\items\\WCS\\connections\\items\\([^=^\\]+)\\"), + Rule("connections", r"^ows\\items\\wcs\\connections\\items\\([^=^\\]+)\\"), + Rule("qgis", r"WCS\\([^=^\\]+)\\"), # only authcfg, password and username + # only authcfg, password and username at "connections\WCS", the rest is at "connections-wcs" + Rule("qgis", r"^connections\\WCS\\([^=^\\]+)\\"), + Rule("qgis", r"^connections-wcs\\([^=^\\]+)\\"), + ], + "XYZ Tiles": [ + Rule("connections", r"^xyz\\items\\([^=^\\]+)\\"), + Rule("qgis", r"^connections-xyz\\([^=^\\]+)\\"), + ], + "ArcGIS-REST-Server": [ + # both feature and map servers as using "featureserver" as defined below... + Rule("qgis", r"^ARCGISFEATURESERVER\\([^=^\\]+)\\"), + Rule("qgis", r"^connections-arcgisfeatureserver\\([^=^\\]+)\\"), + Rule("connections", r"^arcgisfeatureserver\\items\\([^=^\\]+)\\"), + ], + "Scenes": [ + Rule("connections", r"^tiled-scene\\items([^=^\\]+)\\"), + ], + "SensorThings": [ + Rule("connections", r"^sensorthings\\items([^=^\\]+)\\"), + ], +} + + +def collect_data_sources_of_provider( + ini_path: Path, provider: str +) -> dict[str, dict[str, dict[str, str]]]: + """Returns all data source connections of the specified provider in the INI file. + + For example: + { + "data_source_name1": { + "section1": { + "option1": "value1", + "option2": "value2", + ... + }, + "section2": { + ... + }, + }, + "data_source_name2": { + ... + }, + . + } + + Args: + ini_path (str): Path of the INI file to read + provider (str): Name of the provider to gather connections of + + Returns: + Discovered data source connections with their sections, options and values + + Raises: + NotImplementedError: If the provider name is not (yet) known here + """ + search_rules = DATA_SOURCE_SEARCH_RULES.get(provider) + if not search_rules: + raise NotImplementedError(f"Unknown provider: {provider}") + + ini_parser = RawConfigParser() + ini_parser.optionxform = str # str = case-sensitive option names + ini_parser.read(ini_path) + + data_sources = {} + for rule in search_rules: + # multiple rules might reference the same section, but different entries in it + section = rule.section + if not ini_parser.has_section(section): + continue + + regex = compile(rule.regex) + + for option in ini_parser.options(section): + match = regex.search(option) + if match: + # data source name = matching the () group in the regexs + # unquoting is needed for e.g. %20 or umlauts, latin-1 is the correct encoding + # TODO Emoji, e.g. "💩" are not rendered well, also fail to import to other profile... + data_source_name = unquote(match.group(1), "latin-1") + + value = ini_parser.get(section, option) + + # TODO lol clean this up? :) Some modern, typed data structure would be nice. + if not data_sources.get(data_source_name): + data_sources[data_source_name] = {} + if not data_sources[data_source_name].get(section): + data_sources[data_source_name][section] = {} + data_sources[data_source_name][section][option] = value + + LOGGER.info(f"Found {len(data_sources)} {provider!r} data sources") + return data_sources + + +def collect_data_sources( + ini_path: Path, +) -> dict[str, dict[str, dict[str, dict[str, str]]]]: + """Collect all data sources and their ini entries (=options and values inside sections)""" + LOGGER.info(f"Collecting data sources from {ini_path}") + start_time = datetime.now() + + all_data_sources = {} + for provider in DATA_SOURCE_SEARCH_RULES.keys(): + data_source_connections = collect_data_sources_of_provider(ini_path, provider) + all_data_sources[provider] = data_source_connections + # -> provider -> data source -> section -> option -> value + + time_taken = datetime.now() - start_time + LOGGER.debug( + f"Collecting data sources from {ini_path} took {time_taken.microseconds/1000} ms" + ) + + return all_data_sources + + +def import_data_sources( + qgis_ini_file: Path, + data_sources_to_be_imported: dict[str, list[str]], + available_data_sources: dict[str, dict[str, dict[str, dict[str, str]]]], +): + """Import data sources to a QGIS3.ini file. + + Args: + qgis_ini_file: Path of the QGIS3.ini file to import data sources to + data_sources_to_be_imported: Provider name -> List of data source names to import + available_data_sources: Available data sources, will be used find which options and + values have to be imported in which sections. + Providers -> Data sources -> Sections -> Options -> Values + """ + LOGGER.info( + ( + f"Importing {sum([len(v) for v in data_sources_to_be_imported.values()])} " + f"data sources to {qgis_ini_file}" + ) + ) + start_time = datetime.now() + + parser = RawConfigParser() + parser.optionxform = str # str = case-sensitive option names + parser.read(qgis_ini_file) + + for provider, data_sources in data_sources_to_be_imported.items(): + for data_source in data_sources: + LOGGER.info(f"Importing {provider!r}: {data_source!r}") + + # fetch sections+options for import + sections = available_data_sources[provider][data_source] + + for section, options in sections.items(): + LOGGER.debug(f"Importing {section=}, {options=}") + if not parser.has_section(section): + parser.add_section(section) + for option, value in options.items(): + parser.set(section, option, value) + + with open(qgis_ini_file, "w") as qgisconf: + parser.write(qgisconf, space_around_delimiters=False) + + time_taken = datetime.now() - start_time + LOGGER.debug( + f"Importing data sources to {qgis_ini_file} took {time_taken.microseconds / 1000} ms" + ) + + +def remove_data_sources( + qgis_ini_file: Path, + data_sources_to_be_removed: dict[str, list[str]], + available_data_sources: dict[str, dict[str, dict[str, dict[str, str]]]], +): + """Handles data source removal from file + + Args: + qgis_ini_file: Path of the QGIS3.ini file to remove data sources from + data_sources_to_be_removed: Provider name -> List of data source names to remove + available_data_sources: Available data sources, will be used find which options in + which sections have to be removed. + Providers -> Data sources -> Sections -> Options + """ + LOGGER.info( + ( + f"Removing {sum([len(v) for v in data_sources_to_be_removed.values()])} " + f"data sources from {qgis_ini_file}" + ) + ) + start_time = datetime.now() + + parser = RawConfigParser() + parser.optionxform = str # str = case-sensitive option names + parser.read(qgis_ini_file) + + for provider, data_sources in data_sources_to_be_removed.items(): + for data_source in data_sources: + LOGGER.info(f"Removing {provider!r}: {data_source!r}") + + # fetch sections+options for deletion + sections = available_data_sources[provider][data_source] + + for section, options in sections.items(): + LOGGER.debug(f"Removing {section=}, {options=}") + for option in options.keys(): + parser.remove_option(section, option) + + with open(qgis_ini_file, "w") as qgisconf: + parser.write(qgisconf, space_around_delimiters=False) + + time_taken = datetime.now() - start_time + LOGGER.debug( + f"Removing data sources from {qgis_ini_file} took {time_taken.microseconds / 1000} ms" + ) diff --git a/profile_manager/datasources/dataservices/__init__.py b/profile_manager/datasources/dataservices/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile_manager/datasources/dataservices/datasource_distributor.py b/profile_manager/datasources/dataservices/datasource_distributor.py deleted file mode 100644 index 5f0417c..0000000 --- a/profile_manager/datasources/dataservices/datasource_distributor.py +++ /dev/null @@ -1,255 +0,0 @@ -from configparser import RawConfigParser -from urllib.parse import quote - -from profile_manager.utils import adjust_to_operating_system - -KNOWN_WEB_SOURCES = [ - "Vector-Tile", - "WMS", - "WFS", - "WCS", - "XYZ", - "ArcGisMapServer", - "ArcGisFeatureServer", - "GeoNode", -] # must match the names in data source provider's DATA_SOURCE_SEARCH_LOCATIONS. TODO re-use a single rules object! - - -def import_data_sources( - source_qgis_ini_file: str, - target_qgis_ini_file: str, - dictionary_of_checked_web_sources: dict, - dictionary_of_checked_database_sources: dict, -): - """Handles data source import""" - dictionary_of_checked_sources = { - **dictionary_of_checked_database_sources, - **dictionary_of_checked_web_sources, - } - - if dictionary_of_checked_sources: - source_qgis_ini_file = adjust_to_operating_system(source_qgis_ini_file) - target_qgis_ini_file = adjust_to_operating_system(target_qgis_ini_file) - - source_ini_parser = RawConfigParser() - source_ini_parser.optionxform = str # str = case-sensitive option names - source_ini_parser.read(source_qgis_ini_file) - - target_ini_parser = RawConfigParser() - target_ini_parser.optionxform = str # str = case-sensitive option names - target_ini_parser.read(target_qgis_ini_file) - - for key in dictionary_of_checked_sources: - iterator = 0 - - if key in KNOWN_WEB_SOURCES: - if source_ini_parser.has_section("qgis"): - for element in range(len(dictionary_of_checked_web_sources[key])): - import_web_sources( - source_ini_parser, - target_ini_parser, - dictionary_of_checked_web_sources, - key, - iterator, - ) - iterator += 1 - else: - # seems to be a database source - for element in range(len(dictionary_of_checked_database_sources[key])): - import_db_sources( - source_ini_parser, - target_ini_parser, - dictionary_of_checked_database_sources, - key, - iterator, - ) - iterator += 1 - - with open(target_qgis_ini_file, "w") as qgisconf: - target_ini_parser.write(qgisconf, space_around_delimiters=False) - - -def remove_data_sources( - qgis_ini_file: str, - dictionary_of_checked_web_sources: dict, - dictionary_of_checked_database_sources: dict, -): - """Handles data source removal from file""" - dictionary_of_checked_sources = { - **dictionary_of_checked_database_sources, - **dictionary_of_checked_web_sources, - } - - qgis_ini_file = adjust_to_operating_system(qgis_ini_file) - - if dictionary_of_checked_sources: - parser = RawConfigParser() - parser.optionxform = str # str = case-sensitive option names - parser.read(qgis_ini_file) - - for key in dictionary_of_checked_sources: - iterator = 0 - - if key in KNOWN_WEB_SOURCES: - if parser.has_section("qgis"): - for element in range(len(dictionary_of_checked_web_sources[key])): - remove_web_sources( - parser, dictionary_of_checked_web_sources, key, iterator - ) - iterator += 1 - else: - # seems to be a database source - for element in range(len(dictionary_of_checked_database_sources[key])): - remove_db_sources( - parser, dictionary_of_checked_database_sources, key, iterator - ) - iterator += 1 - - with open(qgis_ini_file, "w") as qgisconf: - parser.write(qgisconf, space_around_delimiters=False) - - -def import_web_sources( - source_ini_parser: RawConfigParser, - target_ini_parser: RawConfigParser, - dictionary_of_checked_web_sources: dict, # TODO specify internal structure, TODO rename - key: str, - iterator: int, -): - """Imports web source strings to target file""" - - # FIXME - # The code below uses the user-visible titles of data source groups for looking up the corresponding lines - # in the INI file. This obviously fails if we want to use nicely readable titles in the GUI. - # The following replacement is a temporary workaround to allow this without too much of a refactor. - # To be fixed when https://github.com/WhereGroup/profile-manager/issues/7 is solved. - key_replacements = { - "WMS/WMTS": "WMS", - "WFS / OGC API - Features": "WFS", - "XYZ Tiles": "XYZ", - } - - # get the whole qgis section - to_be_imported_dictionary_sources = dict(source_ini_parser.items("qgis")) - - # filter to all entries matching the provider key (e. g. wms) - to_be_imported_dictionary_sources = dict( - # FIXME store the key to lookup separately to allow different GUI display vs technical implementation - filter( - lambda item: str("connections-" + key.lower()) in item[0], - to_be_imported_dictionary_sources.items(), - ) - ) - - # filter to all remaining entries matching the data source name - to_be_imported_dictionary_sources = dict( - filter( - lambda item: "\\" - + quote(dictionary_of_checked_web_sources[key][iterator].encode("latin-1")) - + "\\" - in item[0], - to_be_imported_dictionary_sources.items(), - ) - ) - - for data_source in to_be_imported_dictionary_sources: - if not target_ini_parser.has_section("qgis"): - target_ini_parser["qgis"] = {} - target_ini_parser.set( - "qgis", data_source, to_be_imported_dictionary_sources[data_source] - ) - - -def import_db_sources( - source_ini_parser: RawConfigParser, - target_ini_parser: RawConfigParser, - dictionary_of_checked_database_sources: dict, # TODO specify internal structure, TODO rename - key: str, - iterator: int, -): - """Imports data base strings to target file""" - - # filter to all entries matching the provider key (e. g. PostgreSQL) - to_be_imported_dictionary_sources = dict(source_ini_parser.items(key)) - - # filter to all remaining entries matching the data source name - to_be_imported_dictionary_sources = dict( - filter( - lambda item: "\\" - + quote( - dictionary_of_checked_database_sources[key][iterator].encode("latin-1") - ) - + "\\" - in item[0], - to_be_imported_dictionary_sources.items(), - ) - ) - - for data_source in to_be_imported_dictionary_sources: - if not target_ini_parser.has_section(key): - target_ini_parser[key] = {} - target_ini_parser.set( - key, data_source, to_be_imported_dictionary_sources[data_source] - ) - - -def remove_web_sources( - parser: RawConfigParser, - dictionary_of_checked_web_sources: dict, # TODO specify internal structure, TODO rename - key: str, - iterator: int, -): - """Removes web source strings from target file""" - - # get the whole qgis section - to_be_deleted_dictionary_sources = dict(parser.items("qgis")) - - # filter to all entries matching the provider key (e. g. wms) - to_be_deleted_dictionary_sources = dict( - filter( - lambda item: str("connections-" + key.lower()) in item[0], - to_be_deleted_dictionary_sources.items(), - ) - ) - - # filter to all remaining entries matching the data source name - to_be_deleted_dictionary_sources = dict( - filter( - lambda item: "\\" - + quote(dictionary_of_checked_web_sources[key][iterator].encode("latin-1")) - + "\\" - in item[0], - to_be_deleted_dictionary_sources.items(), - ) - ) - - for data_source in to_be_deleted_dictionary_sources: - parser.remove_option("qgis", data_source) - - -def remove_db_sources( - parser: RawConfigParser, - dictionary_of_checked_database_sources: dict, # TODO specify internal structure, TODO rename - key: str, - iterator: int, -): - """Remove data base sources from target file""" - - # filter to all entries matching the provider key (e. g. PostgreSQL) - to_be_deleted_dictionary_sources = dict(parser.items(key)) - - # filter to all remaining entries matching the data source name - to_be_deleted_dictionary_sources = dict( - filter( - lambda item: "\\" - + quote( - dictionary_of_checked_database_sources[key][iterator].encode("latin-1") - ) - + "\\" - in item[0], - to_be_deleted_dictionary_sources.items(), - ) - ) - - for data_source in to_be_deleted_dictionary_sources: - parser.remove_option(key, data_source) diff --git a/profile_manager/datasources/dataservices/datasource_handler.py b/profile_manager/datasources/dataservices/datasource_handler.py deleted file mode 100644 index 4b47311..0000000 --- a/profile_manager/datasources/dataservices/datasource_handler.py +++ /dev/null @@ -1,171 +0,0 @@ -from qgis.PyQt.QtWidgets import QMessageBox - -from profile_manager.datasources.bookmarks.bookmark_handler import import_bookmarks -from profile_manager.datasources.customizations.customization_handler import ( - import_customizations, -) -from profile_manager.datasources.dataservices.datasource_distributor import ( - import_data_sources, - remove_data_sources, -) -from profile_manager.datasources.favourites.favourites_handler import import_favourites -from profile_manager.datasources.functions.function_handler import ( - import_expression_functions, -) -from profile_manager.datasources.models.model_handler import import_models -from profile_manager.datasources.models.script_handler import import_scripts -from profile_manager.datasources.plugins.plugin_handler import PluginHandler -from profile_manager.datasources.styles.style_handler import import_styles -from profile_manager.utils import adjust_to_operating_system - - -class DataSourceHandler: - - def __init__(self, profile_manager_dialog, profile_manager): - self.profile_manager = profile_manager - self.dlg = profile_manager_dialog - self.qgis_path = self.profile_manager.qgis_profiles_path - self.dictionary_of_checked_web_sources = {} - self.dictionary_of_checked_data_base_sources = {} - self.source_profile_path = "" - self.target_profile_path = "" - self.source_qgis_ini_file = "" - self.target_qgis_ini_file = "" - self.source_bookmark_file = "" - self.target_bookmark_file = "" - self.plugin_handler = PluginHandler(self.profile_manager) - - def set_data_sources( - self, dictionary_of_checked_web_sources, dictionary_of_checked_data_base_sources - ): - """Sets data sources""" - self.dictionary_of_checked_web_sources = dictionary_of_checked_web_sources - self.dictionary_of_checked_data_base_sources = ( - dictionary_of_checked_data_base_sources - ) - - def import_all_the_things(self): - # TODO rename - """Handles the whole data import action. - - Returns: - boolean: If errors were encountered. - """ - had_errors = False - - import_data_sources( - self.source_qgis_ini_file, - self.target_qgis_ini_file, - self.dictionary_of_checked_web_sources, - self.dictionary_of_checked_data_base_sources, - ) - - self.profile_manager.update_data_sources( - False - ) # TODO moved from DS_D.import_data_sources, do we really need it?! - - if self.dlg.bookmark_check.isChecked(): - error_message = import_bookmarks( - self.source_bookmark_file, self.target_bookmark_file - ) - if error_message: - had_errors = True - QMessageBox.critical( - None, "Error while importing bookmarks", error_message - ) - - if self.dlg.favourites_check.isChecked(): - error_message = import_favourites( - self.source_qgis_ini_file, self.target_qgis_ini_file - ) - if error_message: - had_errors = True - QMessageBox.critical( - None, "Error while importing favourites", error_message - ) - - if self.dlg.models_check.isChecked(): - import_models( - self.source_profile_path, self.target_profile_path - ) # currently has no error handling - - if self.dlg.scripts_check.isChecked(): - import_scripts( - self.source_profile_path, self.target_profile_path - ) # currently has no error handling - - if self.dlg.styles_check.isChecked(): - error_message = import_styles( - self.source_profile_path, self.target_profile_path - ) - if error_message: - had_errors = True - QMessageBox.critical( - None, "Error while importing styles", error_message - ) - - if self.dlg.functions_check.isChecked(): - error_message = import_expression_functions( - self.source_qgis_ini_file, self.target_qgis_ini_file - ) - if error_message: - had_errors = True - QMessageBox.critical( - None, "Error while importing expression functions", error_message - ) - - if self.dlg.ui_check.isChecked(): - import_customizations( - self.source_profile_path, self.target_profile_path - ) # currently has no error handling - - # TODO why does data source import also import plugins again? - self.plugin_handler.import_selected_plugins() - - return had_errors - - def import_plugins(self): - self.plugin_handler.import_selected_plugins() - - def display_plugins(self, only_for_target_profile=False): - """Displays plugins in treeWidget""" - self.plugin_handler.set_path_files() - self.plugin_handler.populate_plugins_list( - only_for_target_profile=only_for_target_profile - ) - - def remove_datasources_and_plugins(self): - """Handles data removal""" - self.plugin_handler.remove_selected_plugins() - - remove_data_sources( - self.source_qgis_ini_file, - self.dictionary_of_checked_web_sources, - self.dictionary_of_checked_data_base_sources, - ) - - self.profile_manager.update_data_sources( - False - ) # TODO moved from DS_D.remove_data_sources, do we really need it?! - - def set_path_to_files(self, source_profile_name, target_profile_name): - """Sets file paths""" - ini_paths = self.profile_manager.get_ini_paths() - self.source_qgis_ini_file = ini_paths["source"] - self.target_qgis_ini_file = ini_paths["target"] - - self.source_profile_path = adjust_to_operating_system( - self.qgis_path + "/" + source_profile_name + "/" - ) - self.target_profile_path = adjust_to_operating_system( - self.qgis_path + "/" + target_profile_name + "/" - ) - - def set_path_to_bookmark_files(self, source_profile_name, target_profile_name): - """Sets file paths""" - self.source_bookmark_file = adjust_to_operating_system( - self.qgis_path + "/" + source_profile_name + "/" + "bookmarks.xml" - ) - self.target_bookmark_file = adjust_to_operating_system( - self.qgis_path + "/" + target_profile_name + "/" + "bookmarks.xml" - ) diff --git a/profile_manager/datasources/dataservices/datasource_provider.py b/profile_manager/datasources/dataservices/datasource_provider.py deleted file mode 100644 index b0fb689..0000000 --- a/profile_manager/datasources/dataservices/datasource_provider.py +++ /dev/null @@ -1,212 +0,0 @@ -from configparser import RawConfigParser -from re import compile, search -from urllib.parse import unquote - -from qgis.core import Qgis, QgsMessageLog -from qgis.PyQt.QtCore import Qt -from qgis.PyQt.QtWidgets import QTreeWidgetItem - -# TODO document these! can we directly integrate them below somewhere? -SERVICE_NAME_REGEX = compile(r"\\(.*?)\\") -GPKG_SERVICE_NAME_REGEX = compile(r"\\(.+).\\") - -""" -"providername-ish": [ # a list of searching rules, not just one, because qgis changed between versions - { - "section": "section_to_search", # INI section in which to search for regex matches - "regex": "<°((^(-<", # regex to search for in the keys of the section - }, -] -# TODO document the versions of QGIS that are using a specific rule -""" -DATA_SOURCE_SEARCH_LOCATIONS = { - "GeoPackage": [ - { - "section": "providers", - "regex": "^ogr.GPKG.connections.*path", - }, - ], - "SpatiaLite": [ - { - "section": "SpatiaLite", - "regex": "^connections.*sqlitepath", - }, - ], - "PostgreSQL": [ - { - "section": "PostgreSQL", - "regex": "^connections.*host", - }, - ], - "MSSQL": [ - { - "section": "MSSQL", - "regex": "^connections.*host", - }, - ], - "DB2": [ - { - "section": "DB2", - "regex": "^connections.*host", - }, - ], - "Oracle": [ - { - "section": "Oracle", - "regex": "^connections.*host", - }, - ], - "Vector-Tile": [ - { - "section": "qgis", - "regex": "^connections-vector-tile.*url", - }, - ], - "WMS": [ - { - "section": "qgis", - "regex": "^connections-wms.*url", - }, - ], - "WFS": [ - { - "section": "qgis", - "regex": "^connections-wfs.*url", - }, - ], - "WCS": [ - { - "section": "qgis", - "regex": "^connections-wcs.*url", - }, - ], - "XYZ": [ - { - "section": "qgis", - "regex": "^connections-xyz.*url", - }, - ], - "ArcGisMapServer": [ - { - "section": "qgis", - "regex": "^connections-arcgismapserver.*url", - }, - ], - "ArcGisFeatureServer": [ - { - "section": "qgis", - "regex": "^connections-arcgisfeatureserver.*url", - }, - ], - # TODO GeoNode was a core plugin once TODO document? - "GeoNode": [ - { - "section": "qgis", - "regex": "^connections-geonode.*url", - }, - ], -} - - -def get_data_sources_tree( - ini_path: str, provider: str, make_checkable: bool -) -> QTreeWidgetItem: - """Returns a tree of checkable items for all data sources of the specified provider in the INI file. - - The tree contains a checkable item per data source found. - - Args: - ini_path (str): Path to the INI file to read - provider (str): Name of the provider to gather data sources for - make_checkable (bool): Flag to indicate if items should be checkable - - Returns: - QTreeWidgetItem: Tree widget item representing the data sources or None if none were found - """ - data_source_connections = gather_data_source_connections(ini_path, provider) - if not data_source_connections: - QgsMessageLog.logMessage( - f"- 0 {provider} connections found", "Profile Manager", Qgis.Info - ) - return None - else: - QgsMessageLog.logMessage( - f"- {len(data_source_connections)} {provider} connections found", - "Profile Manager", - Qgis.Info, - ) - - tree_root_item = QTreeWidgetItem([provider]) - if make_checkable: - tree_root_item.setFlags( - tree_root_item.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable - ) - - data_source_items = [] - for data_source_connection in data_source_connections: - data_source_item = QTreeWidgetItem([data_source_connection]) - if make_checkable: - data_source_item.setFlags(data_source_item.flags() | Qt.ItemIsUserCheckable) - data_source_item.setCheckState(0, Qt.Unchecked) - data_source_items.append(data_source_item) - - tree_root_item.addChildren(data_source_items) - return tree_root_item - - -def gather_data_source_connections(ini_path: str, provider: str) -> list[str]: - """Returns the names of all data source connections of the specified provider in the INI file. - - Args: - ini_path (str): Path to the INI file to read - provider (str): Name of the provider to gather connections of - - Returns: - list[str]: Names of the found data source connections - - Raises: - NotImplementedError: If the provider name is not (yet) known here - """ - search_rules = DATA_SOURCE_SEARCH_LOCATIONS.get(provider) - if not search_rules: - raise NotImplementedError(f"Unknown provider: {provider}") - - # TODO make iterating if more than 1 rule was found - # TODO how to handle multiple finds? deduplicate? - section_to_search = search_rules[0]["section"] - regex = search_rules[0]["regex"] - - ini_parser = RawConfigParser() - ini_parser.optionxform = str # str = case-sensitive option names - ini_parser.read(ini_path) - - try: - section = ini_parser[section_to_search] - except KeyError: - return None - - data_source_connections = [] - regex_pattern = compile(regex) - for key in section: - if regex_pattern.search(key): - if ( - provider == "GeoPackage" - ): # TODO move this logic/condition into the rules if possible? - source_name_raw = search(GPKG_SERVICE_NAME_REGEX, key) - source_name = ( - source_name_raw.group(0) - .replace("\\GPKG\\connections\\", "") - .replace("\\", "") - ) - else: - source_name_raw = search(SERVICE_NAME_REGEX, key) - source_name = source_name_raw.group(0).replace("\\", "") - # TODO what are the replacements needed for?! - - # TODO "Bing VirtualEarth 💩" is not rendered well, also fails to import to other profile... - source_name = unquote( - source_name, "latin-1" - ) # needed for e.g. %20 in connection names - data_source_connections.append(source_name) - - return data_source_connections diff --git a/profile_manager/datasources/expressions.py b/profile_manager/datasources/expressions.py new file mode 100644 index 0000000..ed27c6e --- /dev/null +++ b/profile_manager/datasources/expressions.py @@ -0,0 +1,40 @@ +from configparser import RawConfigParser +from pathlib import Path + + +def import_expressions(source_qgis_ini_file: Path, target_qgis_ini_file: Path): + """Imports custom expressions from source to target profile. + + Custom expressions are stored in QGIS/QGIS3.ini's [expressions] section, e.g.: + ... + [expressions] + ... + user\test_expression\expression=1 + 1 + user\test_expression\helpText="..." + ... + + Note: This does not handle Python expression functions yet. TODO + + Args: + source_qgis_ini_file (str): Path of source QGIS3.ini file + target_qgis_ini_file (str): Path of target QGIS3.ini file + """ + source_ini_parser = RawConfigParser() + source_ini_parser.optionxform = str # str = case-sensitive option names + source_ini_parser.read(source_qgis_ini_file) + + expressions = dict(source_ini_parser.items("expressions")) + + target_ini_parser = RawConfigParser() + target_ini_parser.optionxform = str # str = case-sensitive option names + target_ini_parser.read(target_qgis_ini_file) + + if not target_ini_parser.has_section("expressions"): + target_ini_parser["expressions"] = {} + + for entry in expressions: + if "expression" in entry or "helpText" in entry: + target_ini_parser.set("expressions", entry, expressions[entry]) + + with open(target_qgis_ini_file, "w") as qgisconf: + target_ini_parser.write(qgisconf, space_around_delimiters=False) diff --git a/profile_manager/datasources/favourites.py b/profile_manager/datasources/favourites.py new file mode 100644 index 0000000..f5b517d --- /dev/null +++ b/profile_manager/datasources/favourites.py @@ -0,0 +1,49 @@ +from configparser import RawConfigParser +from pathlib import Path + + +def import_favourites(source_qgis_ini_file: Path, target_qgis_ini_file: Path): + """Imports browser favourites from source to target profile. + + Favourites are stored in QGIS/QGIS3.ini's [browser] section, e.g.: + ... + [browser] + favourites=/path/to|||My favourite folder!, /tmp/test|||title, ... + ... + + Args: + source_qgis_ini_file: Path of source QGIS3.ini file + target_qgis_ini_file: Path of target QGIS3.ini file + """ + source_ini_parser = RawConfigParser() + source_ini_parser.optionxform = str # str = case-sensitive option names + source_ini_parser.read(source_qgis_ini_file) + + get_favourites = dict(source_ini_parser.items("browser")) + + favourites_to_be_imported = {} + favourites_to_be_preserved = "" + + for entry in get_favourites: + if entry == "favourites": + favourites_to_be_imported[entry] = get_favourites[entry] + + target_ini_parser = RawConfigParser() + target_ini_parser.optionxform = str # str = case-sensitive option names + target_ini_parser.read(target_qgis_ini_file) + + if not target_ini_parser.has_section("browser"): + target_ini_parser["browser"] = {} + elif target_ini_parser.has_option("browser", "favourites"): + favourites_to_be_preserved = target_ini_parser.get("browser", "favourites") + + import_string = favourites_to_be_imported["favourites"].replace( + favourites_to_be_preserved, "" + ) + + target_ini_parser.set( + "browser", "favourites", favourites_to_be_preserved + import_string + ) + + with open(target_qgis_ini_file, "w") as qgisconf: + target_ini_parser.write(qgisconf, space_around_delimiters=False) diff --git a/profile_manager/datasources/favourites/__init__.py b/profile_manager/datasources/favourites/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile_manager/datasources/favourites/favourites_handler.py b/profile_manager/datasources/favourites/favourites_handler.py deleted file mode 100644 index eb26a7c..0000000 --- a/profile_manager/datasources/favourites/favourites_handler.py +++ /dev/null @@ -1,58 +0,0 @@ -from configparser import RawConfigParser - -from qgis.core import Qgis, QgsMessageLog - - -def import_favourites(source_qgis_ini_file, target_qgis_ini_file): - """Imports browser favourites from source to target profile. - - Favourites are stored in QGIS/QGIS3.ini's [browser] section, e.g.: - ... - [browser] - favourites=/path/to|||My favourite folder!, /tmp/test|||title, ... - ... - - Args: - TODO - - Returns: - error_message (str): An error message, if something XML related failed. - """ - source_ini_parser = RawConfigParser() - source_ini_parser.optionxform = str # str = case-sensitive option names - source_ini_parser.read(source_qgis_ini_file) - - try: - get_favourites = dict(source_ini_parser.items("browser")) - - favourites_to_be_imported = {} - favourites_to_be_preserved = "" - - for entry in get_favourites: - if entry == "favourites": - favourites_to_be_imported[entry] = get_favourites[entry] - - target_ini_parser = RawConfigParser() - target_ini_parser.optionxform = str # str = case-sensitive option names - target_ini_parser.read(target_qgis_ini_file) - - if not target_ini_parser.has_section("browser"): - target_ini_parser["browser"] = {} - elif target_ini_parser.has_option("browser", "favourites"): - favourites_to_be_preserved = target_ini_parser.get("browser", "favourites") - - import_string = favourites_to_be_imported["favourites"].replace( - favourites_to_be_preserved, "" - ) - - target_ini_parser.set( - "browser", "favourites", favourites_to_be_preserved + import_string - ) - - with open(target_qgis_ini_file, "w") as qgisconf: - target_ini_parser.write(qgisconf, space_around_delimiters=False) - except Exception as e: - # TODO: It would be nice to have a smaller and more specific try block but until then we except broadly - error = f"{type(e)}: {str(e)}" - QgsMessageLog.logMessage(error, "Profile Manager", level=Qgis.Warning) - return error diff --git a/profile_manager/datasources/functions/__init__.py b/profile_manager/datasources/functions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile_manager/datasources/functions/function_handler.py b/profile_manager/datasources/functions/function_handler.py deleted file mode 100644 index dced917..0000000 --- a/profile_manager/datasources/functions/function_handler.py +++ /dev/null @@ -1,57 +0,0 @@ -from configparser import RawConfigParser - -from qgis.core import Qgis, QgsMessageLog - - -def import_expression_functions(source_qgis_ini_file: str, target_qgis_ini_file: str): - """Imports custom expression functions from source to target profile. - - Custom expression functions are stored in QGIS/QGIS3.ini's [expressions] section, e.g.: - ... - [expressions] - ... - user\test_expression\expression=1 + 1 - user\test_expression\helpText="..." - ... - - Note: This does not handle Python expression functions. - - Args: - source_qgis_ini_file (str): Path to source INI file - target_qgis_ini_file (str): Path to target INI file - - Returns: - error_message (str): An error message, if something failed. - """ - QgsMessageLog.logMessage( - "Importing expression functions...", "Profile Manager", Qgis.Info - ) - - source_ini_parser = RawConfigParser() - source_ini_parser.optionxform = str # str = case-sensitive option names - - source_ini_parser.read(source_qgis_ini_file) - try: - get_functions = dict(source_ini_parser.items("expressions")) - - target_ini_parser = RawConfigParser() - target_ini_parser.optionxform = str # str = case-sensitive option names - target_ini_parser.read(target_qgis_ini_file) - - if not target_ini_parser.has_section("expressions"): - target_ini_parser["expressions"] = {} - - for entry in get_functions: - if "expression" in entry or "helpText" in entry: - target_ini_parser.set("expressions", entry, get_functions[entry]) - QgsMessageLog.logMessage( - f"Found '{entry}'", "Profile Manager", Qgis.Info - ) - - with open(target_qgis_ini_file, "w") as qgisconf: - target_ini_parser.write(qgisconf, space_around_delimiters=False) - except Exception as e: - # TODO: It would be nice to have a smaller and more specific try block but until then we except broadly - error = f"{type(e)}: {str(e)}" - QgsMessageLog.logMessage(error, "Profile Manager", level=Qgis.Warning) - return error diff --git a/profile_manager/datasources/models.py b/profile_manager/datasources/models.py new file mode 100644 index 0000000..7af34ca --- /dev/null +++ b/profile_manager/datasources/models.py @@ -0,0 +1,34 @@ +from os import listdir +from pathlib import Path +from shutil import copy2 + + +def import_models(source_profile_path: Path, target_profile_path: Path): + """Imports Processing models from source to target profile. + + Note: Existing models with identical filenames will be overwritten! + + Models are stored in the processing/models/ subdirectory of a profile, e.g.: + ... + processing/models/my_model.model3 + processing/models/das.model3 + ... + + Args: + source_profile_path: Path of profile directory to import from + target_profile_path: Path of profile directory to import to + """ + source_models_dir = source_profile_path / "processing" / "models" + target_models_dir = target_profile_path / "processing" / "models" + + if not source_models_dir.exists(): + return + if not target_models_dir.exists(): + target_models_dir.mkdir(parents=True, exist_ok=True) + for item in listdir(source_models_dir): + source = source_models_dir / item + dest = target_models_dir / item + if source.is_dir(): + continue + else: + copy2(source, dest) diff --git a/profile_manager/datasources/models/__init__.py b/profile_manager/datasources/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile_manager/datasources/models/model_handler.py b/profile_manager/datasources/models/model_handler.py deleted file mode 100644 index 4d42917..0000000 --- a/profile_manager/datasources/models/model_handler.py +++ /dev/null @@ -1,34 +0,0 @@ -from os import listdir, path -from pathlib import Path -from shutil import copy2 - - -def import_models(source_profile_path: str, target_profile_path: str): - """Imports Processing models from source to target profile. - - Note: Existing models with identical filenames will be overwritten! - - Models are stored in the processing/models/ subdirectory of a profile, e.g.: - ... - processing/models/my_model.model3 - processing/models/das.model3 - ... - - Args: - TODO - """ - source_models_dir = source_profile_path + "processing/models/" - target_models_dir = target_profile_path + "processing/models/" - - if path.exists(source_models_dir): - if not path.exists(target_models_dir): - Path(target_models_dir).mkdir(parents=True, exist_ok=True) - for item in listdir(source_models_dir): - source = path.join(source_models_dir, item) - dest = path.join(target_models_dir, item) - if path.isdir(source): - continue - else: - copy2(source, dest) - else: - pass diff --git a/profile_manager/datasources/models/script_handler.py b/profile_manager/datasources/models/script_handler.py deleted file mode 100644 index b59fc80..0000000 --- a/profile_manager/datasources/models/script_handler.py +++ /dev/null @@ -1,33 +0,0 @@ -from os import listdir, path -from pathlib import Path -from shutil import copy2 - - -def import_scripts(source_profile_path: str, target_profile_path: str): - """Imports Processing scripts from source to target profile. - - Note: Existing scripts with identical filenames will be overwritten! - - Scripts are stored in the processing/scripts/ subdirectory of a profile, e.g.: - ... - processing/scripts/my_great_processing_script.py - processing/scripts/snakes.py - ... - - Args: - TODO - """ - source_scripts_dir = source_profile_path + "processing/scripts/" - target_scripts_dir = target_profile_path + "processing/scripts/" - if path.exists(source_scripts_dir): - if not path.exists(target_scripts_dir): - Path(target_scripts_dir).mkdir(parents=True, exist_ok=True) - for item in listdir(source_scripts_dir): - source = path.join(source_scripts_dir, item) - dest = path.join(target_scripts_dir, item) - if path.isdir(source): - continue - else: - copy2(source, dest) - else: - pass diff --git a/profile_manager/datasources/plugins.py b/profile_manager/datasources/plugins.py new file mode 100644 index 0000000..4c8bdbe --- /dev/null +++ b/profile_manager/datasources/plugins.py @@ -0,0 +1,150 @@ +import logging + +from configparser import NoSectionError, RawConfigParser +from datetime import datetime +from pathlib import Path +from shutil import rmtree, copytree + + +LOGGER = logging.getLogger("profile_manager") + + +# Via QGIS/python/plugins/CMakeLists.txt +CORE_PLUGINS = [ + "db_manager", + "GdalTools", # not a plugin anymore since QGIS 3.0 + "grassprovider", # plugin since 3.22 + "MetaSearch", + "otbprovider", # plugin since 3.22 + "processing", + "sagaprovider", # removed in 3.30 +] + + +def collect_plugin_names(qgis_ini_file: Path) -> list[str]: + # TODO use ini AND file system, ini might have empty leftovers... + LOGGER.info(f"Collecting plugin names from {qgis_ini_file}") + start_time = datetime.now() + + ini_parser = RawConfigParser() + ini_parser.optionxform = str # str = case-sensitive option names + ini_parser.read(qgis_ini_file) + + try: + plugins_in_profile = ini_parser.options("PythonPlugins") + except NoSectionError: + LOGGER.warning(f"No plugins found in {qgis_ini_file}!") + plugins_in_profile = [] + + time_taken = datetime.now() - start_time + LOGGER.debug( + f"Collecting plugin names from {qgis_ini_file} took {time_taken.microseconds/1000} ms" + ) + + return plugins_in_profile + + +def import_plugins( + source_profile_path: Path, + target_profile_path: Path, + target_qgis_ini_file: Path, + plugin_names: list[str], +): + """Copies the specified plugins from source to target profile. + + Copies the files and sets the INI options accordingly. + Imported plugins are always set to be active. + + Note: Plugin specific settings are not copied as we have no way of knowing where or how they are stored. + + Plugins are stored in python/plugins/ + Their active state is tracked in QGIS/QGIS3.ini's [PythonPlugins] section, e.g.: + ... + [PythonPlugins] + ... + fooPlugin=true + PluggyBar=true + BaZ=false + ... + + Args: + source_profile_path: Path of profile directory to import from + target_profile_path: Path of profile directory to import to + target_qgis_ini_file: Path of target QGIS3.ini file to import to + plugin_names: List of plugins (names according to QGIS3.ini) to import + """ + LOGGER.info(f"Importing {len(plugin_names)} data sources to {target_profile_path}") + start_time = datetime.now() + + ini_parser = RawConfigParser() + ini_parser.optionxform = str # str = case-sensitive option names + ini_parser.read(target_qgis_ini_file) + + if not ini_parser.has_section("PythonPlugins"): + ini_parser["PythonPlugins"] = {} + + for plugin_name in plugin_names: + ini_parser.set("PythonPlugins", plugin_name, "true") + + source_plugin_dir = source_profile_path / "python" / "plugins" / plugin_name + target_plugins_dir = target_profile_path / "python" / "plugins" + target_plugin_dir = target_plugins_dir / plugin_name + + if source_plugin_dir.exists(): + if not target_plugins_dir.exists(): # TODO necessary? + target_plugins_dir.mkdir(parents=True, exist_ok=True) + if not target_plugin_dir.is_dir(): + copytree(source_plugin_dir, target_plugin_dir) + else: + continue # TODO error, dont skip silently! + + with open(target_qgis_ini_file, "w") as qgisconf: + ini_parser.write(qgisconf, space_around_delimiters=False) + + time_taken = datetime.now() - start_time + LOGGER.debug( + f"Importing plugins to {target_profile_path} took {time_taken.microseconds / 1000} ms" + ) + + +def remove_plugins( + profile_path: Path, + qgis_ini_file: Path, + plugin_names: list[str], +): + """Removes the specified plugins from the profile. + + Removes both the files from python/plugins/ and the QGIS/QGIS3.ini [PythonPlugins] section entries. + + Note: Plugin-specific *settings* are not removed as we have no way of knowing where or how they are stored. + + Args: + profile_path: Path of profile directory to remove from + qgis_ini_file: Path of target QGIS3.ini file to remove from + plugin_names: List of plugins (names according to QGIS3.ini) to remove + """ + LOGGER.info(f"Removing {len(plugin_names)} data sources from {profile_path}") + start_time = datetime.now() + + ini_parser = RawConfigParser() + ini_parser.optionxform = str # str = case-sensitive option names + ini_parser.read(qgis_ini_file) + + for plugin_name in plugin_names: + if plugin_name in CORE_PLUGINS: + continue + # Remove plugin from active state list in PythonPlugins section + if ini_parser.has_option("PythonPlugins", plugin_name): + ini_parser.remove_option("PythonPlugins", plugin_name) + + # Remove plugin dir + plugins_dir = profile_path / "python" / "plugins" / plugin_name + rmtree(plugins_dir) + + with open(qgis_ini_file, "w") as qgisconf: + ini_parser.write(qgisconf, space_around_delimiters=False) + + time_taken = datetime.now() - start_time + LOGGER.debug( + f"Removing plugins from {profile_path} took {time_taken.microseconds / 1000} ms" + ) diff --git a/profile_manager/datasources/plugins/__init__.py b/profile_manager/datasources/plugins/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile_manager/datasources/plugins/plugin_displayer.py b/profile_manager/datasources/plugins/plugin_displayer.py deleted file mode 100644 index 022a7d9..0000000 --- a/profile_manager/datasources/plugins/plugin_displayer.py +++ /dev/null @@ -1,71 +0,0 @@ -from configparser import NoSectionError, RawConfigParser - -from qgis.PyQt.QtCore import Qt -from qgis.PyQt.QtWidgets import QListWidgetItem - - -class PluginDisplayer: - - def __init__(self, profile_manager): - self.profile_manager = profile_manager - self.source_qgis_ini_file = "" - self.target_qgis_ini_file = "" - - # Via QGIS/python/plugins/CMakeLists.txt - self.core_plugins = [ - "db_manager", - "GdalTools", # not a plugin anymore since QGIS 3.0 - "grassprovider", # plugin since 3.22 - "MetaSearch", - "otbprovider", # plugin since 3.22 - "processing", - "sagaprovider", # removed in 3.30 - ] - - def populate_plugins_list(self, only_populate_target_profile=False): - """Gets plugins from ini file and add them to treeWidget using their directory name - - Args: - only_populate_target_profile (bool): If only the target list should be populated - """ - ini_parser = RawConfigParser() - ini_parser.optionxform = str # str = case-sensitive option names - - if only_populate_target_profile: - ini_parser.read(self.target_qgis_ini_file) - plugin_list_widget = self.profile_manager.dlg.list_plugins_target - else: - ini_parser.read(self.source_qgis_ini_file) - plugin_list_widget = self.profile_manager.dlg.list_plugins - - plugin_list_widget.clear() - - try: - plugins_in_profile = dict(ini_parser.items("PythonPlugins")) - except NoSectionError: - ini_parser["PythonPlugins"] = {} - plugins_in_profile = dict(ini_parser.items("PythonPlugins")) - - # add an item to the list for each non-core plugin - for plugin_name in plugins_in_profile: - item = QListWidgetItem() - - if not only_populate_target_profile: - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) - item.setCheckState(Qt.Unchecked) - else: - item.setFlags(item.flags() & ~Qt.ItemIsSelectable) - - if plugin_name in self.core_plugins: - item.setFlags(item.flags() & ~Qt.ItemIsEnabled) - plugin_name = f"{plugin_name} (Core Plugin)" - - item.setText(str(plugin_name)) - plugin_list_widget.addItem(item) - - if not only_populate_target_profile: - self.populate_plugins_list(only_populate_target_profile=True) - - def set_ini_paths(self, source, target): - self.source_qgis_ini_file = source - self.target_qgis_ini_file = target diff --git a/profile_manager/datasources/plugins/plugin_handler.py b/profile_manager/datasources/plugins/plugin_handler.py deleted file mode 100644 index 41f2cb6..0000000 --- a/profile_manager/datasources/plugins/plugin_handler.py +++ /dev/null @@ -1,65 +0,0 @@ -from qgis.PyQt.QtCore import Qt - -from profile_manager.datasources.plugins.plugin_displayer import PluginDisplayer -from profile_manager.datasources.plugins.plugin_importer import import_plugins -from profile_manager.datasources.plugins.plugin_remover import remove_plugins - - -class PluginHandler: - - def __init__(self, profile_manager): - self.profile_manager = profile_manager - self.plugin_displayer = PluginDisplayer(self.profile_manager) - self.source_qgis_ini_file = "" - self.target_qgis_ini_file = "" - - def populate_plugins_list(self, only_for_target_profile=False): - """Gets active plugins from ini file and displays them in treeWidget""" - self.set_path_files() - self.plugin_displayer.set_ini_paths( - self.source_qgis_ini_file, self.target_qgis_ini_file - ) - self.plugin_displayer.populate_plugins_list( - only_populate_target_profile=only_for_target_profile - ) - - def import_selected_plugins(self): - """Import selected plugins into target profile""" - source_profile_path, target_profile_path = ( - self.profile_manager.get_profile_paths() - ) - - plugin_names = [] - for item in self.profile_manager.dlg.list_plugins.findItems( - "", Qt.MatchContains | Qt.MatchRecursive - ): - if item.checkState() == Qt.Checked: - plugin_names.append(item.text()) - - import_plugins( - source_profile_path, - target_profile_path, - self.target_qgis_ini_file, - plugin_names, - ) - self.populate_plugins_list() - - def remove_selected_plugins(self): - """Removes selected plugins from source profile""" - source_profile_path, _ = self.profile_manager.get_profile_paths() - - plugin_names = [] - for item in self.profile_manager.dlg.list_plugins.findItems( - "", Qt.MatchContains | Qt.MatchRecursive - ): - if item.checkState() == Qt.Checked: - plugin_names.append(item.text()) - - remove_plugins(source_profile_path, self.source_qgis_ini_file, plugin_names) - self.populate_plugins_list() - - def set_path_files(self): - """Sets file paths""" - ini_paths = self.profile_manager.get_ini_paths() - self.source_qgis_ini_file = ini_paths["source"] - self.target_qgis_ini_file = ini_paths["target"] diff --git a/profile_manager/datasources/plugins/plugin_importer.py b/profile_manager/datasources/plugins/plugin_importer.py deleted file mode 100644 index fd1b2e1..0000000 --- a/profile_manager/datasources/plugins/plugin_importer.py +++ /dev/null @@ -1,63 +0,0 @@ -from configparser import RawConfigParser -from os import path -from pathlib import Path -from shutil import copytree - -from profile_manager.utils import adjust_to_operating_system - - -def import_plugins( - source_profile_path: str, - target_profile_path: str, - target_qgis_ini_file: str, - plugin_names: list[str], -): - """Copies the specified plugins from source to target profile. - - Copies the files and sets the INI options accordingly. - Imported plugins are always set to be active. - - Note: Plugin specific settings are not copied as we have no way of knowing where or how they are stored. - - Plugins are stored in python/plugins/ - Their active state is tracked in QGIS/QGIS3.ini's [PythonPlugins] section, e.g.: - ... - [PythonPlugins] - ... - fooPlugin=true - PluggyBar=true - BaZ=false - ... - - Args: - TODO - """ - ini_parser = RawConfigParser() - ini_parser.optionxform = str # str = case-sensitive option names - ini_parser.read(target_qgis_ini_file) - - if not ini_parser.has_section("PythonPlugins"): - ini_parser["PythonPlugins"] = {} - - for plugin_name in plugin_names: - ini_parser.set("PythonPlugins", plugin_name, "true") - - source_plugin_dir = adjust_to_operating_system( - source_profile_path + "python/plugins/" + plugin_name + "/" - ) - target_plugin_dir = adjust_to_operating_system( - target_profile_path + "python/plugins/" + plugin_name + "/" - ) - - if path.exists(source_plugin_dir): - if not path.exists(target_profile_path + "python/plugins/"): - Path(target_profile_path + "python/plugins/").mkdir( - parents=True, exist_ok=True - ) - if not path.isdir(target_plugin_dir): - copytree(source_plugin_dir, target_plugin_dir) - else: - continue # TODO error, dont skip silently! - - with open(target_qgis_ini_file, "w") as qgisconf: - ini_parser.write(qgisconf, space_around_delimiters=False) diff --git a/profile_manager/datasources/plugins/plugin_remover.py b/profile_manager/datasources/plugins/plugin_remover.py deleted file mode 100644 index f79d4fd..0000000 --- a/profile_manager/datasources/plugins/plugin_remover.py +++ /dev/null @@ -1,50 +0,0 @@ -from configparser import RawConfigParser -from shutil import rmtree - -from qgis.PyQt.QtWidgets import QMessageBox - -from profile_manager.utils import adjust_to_operating_system, tr - - -def remove_plugins( - profile_path: str, - qgis_ini_file: str, - plugin_names: list[str], -): - """Removes the specified plugins from the profile. - - Removes both the files from python/plugins/ and the QGIS/QGIS3.ini [PythonPlugins] section entries. - - Note: Plugin specific settings are not removed as we have no way of knowing where or how they are stored. - - Args: - TODO - """ - ini_parser = RawConfigParser() - ini_parser.optionxform = str # str = case-sensitive option names - ini_parser.read(qgis_ini_file) - - for plugin_name in plugin_names: - # Removes plugin from active state list in PythonPlugins section - if ini_parser.has_option("PythonPlugins", plugin_name): - ini_parser.remove_option("PythonPlugins", plugin_name) - - plugins_dir = adjust_to_operating_system( - profile_path + "python/plugins/" + plugin_name + "/" - ) - - try: - rmtree(plugins_dir) - except OSError as e: - # TODO do not do GUI stuff in these functions if possible, maybe return a list of errors instead? - QMessageBox.critical( - None, - tr("Plugin could not be removed"), - tr("Plugin '{0}' could not be removed due to error:\n{1}").format( - plugin_name, e - ), - ) - continue - - with open(qgis_ini_file, "w") as qgisconf: - ini_parser.write(qgisconf, space_around_delimiters=False) diff --git a/profile_manager/datasources/scripts.py b/profile_manager/datasources/scripts.py new file mode 100644 index 0000000..940ec08 --- /dev/null +++ b/profile_manager/datasources/scripts.py @@ -0,0 +1,33 @@ +from os import listdir +from pathlib import Path +from shutil import copy2 + + +def import_scripts(source_profile_path: Path, target_profile_path: Path): + """Imports Processing scripts from source to target profile. + + Note: Existing scripts with identical filenames will be overwritten! + + Scripts are stored in the processing/scripts/ subdirectory of a profile, e.g.: + ... + processing/scripts/my_great_processing_script.py + processing/scripts/snakes.py + ... + + Args: + source_profile_path: Path of profile directory to import from + target_profile_path: Path of profile directory to import to + """ + source_scripts_dir = source_profile_path / "processing" / "scripts" + target_scripts_dir = target_profile_path / "processing" / "scripts" + if not source_scripts_dir.exists(): + return + if not target_scripts_dir.exists(): + target_scripts_dir.mkdir(parents=True, exist_ok=True) + for item in listdir(source_scripts_dir): + source = source_scripts_dir / item + dest = target_scripts_dir / item + if source.is_dir(): + continue + else: + copy2(source, dest) diff --git a/profile_manager/datasources/styles.py b/profile_manager/datasources/styles.py new file mode 100644 index 0000000..c1741ca --- /dev/null +++ b/profile_manager/datasources/styles.py @@ -0,0 +1,50 @@ +import sqlite3 +from pathlib import Path +from shutil import copy + + +def import_styles(source_profile_path: Path, target_profile_path: Path): + """Imports styles from source profile to target profile. + + Note: Currently it only imports symbols (not 3D) and label settings, not 3D symbols, color ramps, tags, etc. + + Styles are stored in symbology-style.db. + + Args: + source_profile_path: Path of profile directory to import from + target_profile_path: Path of profile directory to import to + """ + source_db_path = source_profile_path / "symbology-style.db" + target_db_path = target_profile_path / "symbology-style.db" + + # try if we can straight up copy the file to the target profile + if not target_db_path.is_file(): + copy(source_db_path, target_db_path) + return + + # target file exists, so we transfer styles via SQL + source_db = sqlite3.connect(source_db_path) + target_db = sqlite3.connect(target_db_path) + + source_db_cursor = source_db.cursor() + target_db_cursor = target_db.cursor() + + # import label settings + custom_labels = source_db_cursor.execute("SELECT * FROM labelsettings") + target_db_cursor.executemany( + "INSERT OR REPLACE INTO labelsettings VALUES (?, ?, ?, ?)", custom_labels + ) + + # import symbols + # FIXME: This has a hard-coded assumption that symbols with ids <= 115 are builtin symbols, + # this will fail as soon as a new builtin symbol is shipped by QGIS. + custom_symbols = source_db_cursor.execute("SELECT * FROM symbol WHERE id > 115") + target_db_cursor.executemany( + "INSERT OR REPLACE INTO symbol VALUES (?, ?, ?, ?)", custom_symbols + ) + + source_db.commit() + target_db.commit() + + source_db.close() + target_db.close() diff --git a/profile_manager/datasources/styles/__init__.py b/profile_manager/datasources/styles/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile_manager/datasources/styles/style_handler.py b/profile_manager/datasources/styles/style_handler.py deleted file mode 100644 index 6e6c9b0..0000000 --- a/profile_manager/datasources/styles/style_handler.py +++ /dev/null @@ -1,58 +0,0 @@ -import sqlite3 -from os import path -from shutil import copy - -from qgis.core import Qgis, QgsMessageLog - - -def import_styles(source_profile_path: str, target_profile_path: str): - """Imports styles from source profile to target profile. - - Note: Currently it only imports symbols (not 3D) and label settings, not 3D symbols, color ramps, tags, etc. - - Styles are stored in symbology-style.db. - - Args: - TODO - - Returns: - error_message (str): An error message, if something SQL related failed. - """ - - source_db_path = source_profile_path + "symbology-style.db" - target_db_path = target_profile_path + "symbology-style.db" - - if not path.isfile(target_db_path): - copy(source_db_path, target_db_path) - return - - source_db = sqlite3.connect(source_db_path) - target_db = sqlite3.connect(target_db_path) - - source_db_cursor = source_db.cursor() - target_db_cursor = target_db.cursor() - - try: - # import label settings - custom_labels = source_db_cursor.execute("SELECT * FROM labelsettings") - target_db_cursor.executemany( - "INSERT OR REPLACE INTO labelsettings VALUES (?,?,?,?)", custom_labels - ) - - # import symbols - # FIXME: This has a hard-coded assumption that symbols with ids <= 115 are builtin symbols, - # this will fail as soon as a new builtin symbol is shipped by QGIS. - custom_symbols = source_db_cursor.execute("SELECT * FROM symbol WHERE id>115") - target_db_cursor.executemany( - "INSERT OR REPLACE INTO symbol VALUES (?,?,?,?)", custom_symbols - ) - - source_db.commit() - target_db.commit() - - source_db.close() - target_db.close() - except sqlite3.Error as e: - error = f"{type(e)}: {str(e)}" - QgsMessageLog.logMessage(error, "Profile Manager", level=Qgis.Warning) - return error diff --git a/profile_manager/gui/interface_handler.py b/profile_manager/gui/interface_handler.py deleted file mode 100644 index 934be6d..0000000 --- a/profile_manager/gui/interface_handler.py +++ /dev/null @@ -1,231 +0,0 @@ -from pathlib import Path - -from qgis.core import Qgis, QgsApplication, QgsMessageLog -from qgis.PyQt.QtCore import Qt -from qgis.PyQt.QtWidgets import QDialog - -from profile_manager.datasources.dataservices.datasource_provider import ( - DATA_SOURCE_SEARCH_LOCATIONS, - get_data_sources_tree, -) - - -class InterfaceHandler(QDialog): - - def __init__(self, profile_manager, profile_manager_dialog, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.profile_manager = profile_manager - self.dlg = profile_manager_dialog - self.checked = False - - def populate_data_source_tree(self, profile_name, populating_source_profile): - """Populates the chosen profile's data source tree. - - Args: - profile_name (str): Name of the profile for labelling - populating_source_profile (bool): If the source profile is populated - """ - QgsMessageLog.logMessage( - f"Scanning profile '{profile_name}' for data source connections:", - "Profile Manager", - Qgis.Info, - ) - ini_paths = self.profile_manager.get_ini_paths() - if populating_source_profile: - target_ini_path = ini_paths["source"] - else: - target_ini_path = ini_paths["target"] - - # collect data source tree items from ini file - data_source_list = [] - for provider in DATA_SOURCE_SEARCH_LOCATIONS.keys(): - tree_root_item = get_data_sources_tree( - target_ini_path, provider, make_checkable=populating_source_profile - ) - if tree_root_item: - data_source_list.append(tree_root_item) - QgsMessageLog.logMessage( - f"Scanning profile '{profile_name}' for data source connections: Done!", - "Profile Manager", - Qgis.Info, - ) - - # populate tree - if populating_source_profile: - self.dlg.treeWidgetSource.clear() - self.dlg.treeWidgetSource.setHeaderLabel( - self.tr("Source Profile: {}").format(profile_name) - ) - for tree_root_item in data_source_list: - self.dlg.treeWidgetSource.addTopLevelItem(tree_root_item) - else: - self.dlg.treeWidgetTarget.clear() - self.dlg.treeWidgetTarget.setHeaderLabel( - self.tr("Target Profile: {}").format(profile_name) - ) - for tree_root_item in data_source_list: - self.dlg.treeWidgetTarget.addTopLevelItem(tree_root_item) - - def populate_profile_listings(self): - """Populates the main list as well as the comboboxes with available profile names. - - Also updates button states according to resulting selections. - """ - self.dlg.comboBoxNamesSource.blockSignals(True) - active_profile_name = Path(QgsApplication.qgisSettingsDirPath()).name - - self.dlg.comboBoxNamesSource.setCurrentText(active_profile_name) - - self.dlg.comboBoxNamesSource.blockSignals(False) - self.dlg.comboBoxNamesTarget.blockSignals(False) - self.dlg.list_profiles.blockSignals(False) - self.conditionally_enable_profile_buttons() - - def setup_connections(self): - """Set up connections""" - # buttons - self.dlg.importButton.clicked.connect( - self.profile_manager.import_action_handler - ) - self.dlg.closeDialog.rejected.connect(self.dlg.close) - self.dlg.createProfileButton.clicked.connect( - self.profile_manager.profile_manager_action_handler.create_new_profile - ) - self.dlg.removeProfileButton.clicked.connect( - self.profile_manager.profile_manager_action_handler.remove_profile - ) - self.dlg.removeSourcesButton.clicked.connect( - self.profile_manager.remove_source_action_handler - ) - self.dlg.editProfileButton.clicked.connect( - self.profile_manager.profile_manager_action_handler.edit_profile - ) - self.dlg.copyProfileButton.clicked.connect( - self.profile_manager.profile_manager_action_handler.copy_profile - ) - - # checkbox - self.dlg.checkBox_checkAll.stateChanged.connect(self.check_everything) - - # selections/indexes - self.dlg.comboBoxNamesSource.currentIndexChanged.connect( - lambda: self.profile_manager.update_data_sources(False, True) - ) - self.dlg.comboBoxNamesTarget.currentIndexChanged.connect( - lambda: self.profile_manager.update_data_sources(True, False) - ) - self.dlg.comboBoxNamesSource.currentIndexChanged.connect( - self.conditionally_enable_import_button - ) - self.dlg.comboBoxNamesTarget.currentIndexChanged.connect( - self.conditionally_enable_import_button - ) - self.dlg.list_profiles.selectionModel().selectionChanged.connect( - self.conditionally_enable_profile_buttons - ) - - def check_everything(self): - """Checks/Unchecks every checkbox in the gui""" - if self.checked: - self.uncheck_everything() - else: - for item in self.dlg.treeWidgetSource.findItems( - "", Qt.MatchContains | Qt.MatchRecursive - ): - item.setCheckState(0, Qt.Checked) - - for item in self.dlg.list_plugins.findItems( - "", Qt.MatchContains | Qt.MatchRecursive - ): - item.setCheckState(Qt.Checked) - - self.dlg.bookmark_check.setCheckState(Qt.Checked) - self.dlg.favourites_check.setCheckState(Qt.Checked) - self.dlg.models_check.setCheckState(Qt.Checked) - self.dlg.scripts_check.setCheckState(Qt.Checked) - self.dlg.styles_check.setCheckState(Qt.Checked) - self.dlg.functions_check.setCheckState(Qt.Checked) - self.dlg.ui_check.setChecked(Qt.Checked) - - self.checked = not self.checked - - def uncheck_everything(self): - """Uncheck's every checkbox""" - self.dlg.bookmark_check.setChecked(Qt.Unchecked) - self.dlg.models_check.setChecked(Qt.Unchecked) - self.dlg.favourites_check.setChecked(Qt.Unchecked) - self.dlg.scripts_check.setChecked(Qt.Unchecked) - self.dlg.styles_check.setChecked(Qt.Unchecked) - self.dlg.functions_check.setChecked(Qt.Unchecked) - self.dlg.ui_check.setChecked(Qt.Unchecked) - self.dlg.checkBox_checkAll.setChecked(Qt.Unchecked) - - for item in self.dlg.treeWidgetSource.findItems( - "", Qt.MatchContains | Qt.MatchRecursive - ): - item.setCheckState(0, Qt.Unchecked) - - for iterator in range(self.dlg.list_plugins.count()): - self.dlg.list_plugins.item(iterator).setCheckState(Qt.Unchecked) - - def conditionally_enable_import_button(self): - """Sets up buttons of the Import tab so that the user is not tempted to do "impossible" things. - - Called when profile selection changes in the Import tab. - """ - - # Don't allow import of a profile into itself - if ( - self.dlg.comboBoxNamesSource.currentText() - == self.dlg.comboBoxNamesTarget.currentText() - ): - self.dlg.importButton.setToolTip( - self.tr("Target profile can not be same as source profile") - ) - self.dlg.importButton.setEnabled(False) - else: - self.dlg.importButton.setToolTip("") - self.dlg.importButton.setEnabled(True) - - def conditionally_enable_profile_buttons(self): - """Sets up buttons of the Profiles tab so that the user is not tempted to do "impossible" things. - - Called when profile selection changes in the Profiles tab. - """ - # A profile must be selected - if self.dlg.get_list_selection_profile_name() is None: - self.dlg.removeProfileButton.setToolTip( - self.tr("Please choose a profile to remove") - ) - self.dlg.removeProfileButton.setEnabled(False) - self.dlg.editProfileButton.setToolTip( - self.tr("Please choose a profile to rename") - ) - self.dlg.editProfileButton.setEnabled(False) - self.dlg.copyProfileButton.setToolTip( - self.tr("Please select a profile to copy from") - ) - self.dlg.copyProfileButton.setEnabled(False) - # Some actions can/should not be done on the currently active profile - elif ( - self.dlg.get_list_selection_profile_name() - == Path(QgsApplication.qgisSettingsDirPath()).name - ): - self.dlg.removeProfileButton.setToolTip( - self.tr("The active profile cannot be removed") - ) - self.dlg.removeProfileButton.setEnabled(False) - self.dlg.editProfileButton.setToolTip( - self.tr("The active profile cannot be renamed") - ) - self.dlg.editProfileButton.setEnabled(False) - self.dlg.copyProfileButton.setToolTip("") - self.dlg.copyProfileButton.setEnabled(True) - else: - self.dlg.removeProfileButton.setToolTip("") - self.dlg.removeProfileButton.setEnabled(True) - self.dlg.editProfileButton.setToolTip("") - self.dlg.editProfileButton.setEnabled(True) - self.dlg.copyProfileButton.setToolTip("") - self.dlg.copyProfileButton.setEnabled(True) diff --git a/profile_manager/gui/utils.py b/profile_manager/gui/utils.py new file mode 100644 index 0000000..862696b --- /dev/null +++ b/profile_manager/gui/utils.py @@ -0,0 +1,68 @@ +from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtWidgets import QListWidgetItem, QTreeWidgetItem + +from profile_manager.datasources.plugins import CORE_PLUGINS + + +def data_sources_as_tree( + provider: str, data_sources: dict, make_checkable: bool +) -> QTreeWidgetItem: + """Returns a tree of checkable items for all specified data sources, the root item named by the provider. + + The tree contains a checkable item per data source found. + + Args: + provider: Name of the provider to gather data sources for + data_sources: TODO document structure + make_checkable: Flag to indicate if items should be checkable + + Returns: + QTreeWidgetItem: Tree widget item representing the data sources or None if none were found + """ + + tree_root_item = QTreeWidgetItem([provider]) + if make_checkable: + tree_root_item.setFlags( + tree_root_item.flags() + | Qt.ItemFlag.ItemIsTristate + | Qt.ItemFlag.ItemIsUserCheckable + ) + + data_source_items = [] + for data_source in data_sources: + data_source_item = QTreeWidgetItem([data_source]) + if make_checkable: + data_source_item.setFlags( + data_source_item.flags() | Qt.ItemFlag.ItemIsUserCheckable + ) + data_source_item.setCheckState(0, Qt.CheckState.Unchecked) + data_source_items.append(data_source_item) + + tree_root_item.addChildren(data_source_items) + return tree_root_item + + +def plugins_as_items(plugins: list[str], make_checkable: bool) -> list[QListWidgetItem]: + """Return the plugins as list of QListWidgetItem. + + Core Plugins are specially marked. + """ + items = [] + for plugin_name in plugins: + item = QListWidgetItem() + + if make_checkable: + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(Qt.CheckState.Unchecked) + else: + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable) + + if plugin_name in CORE_PLUGINS: + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEnabled) + item.setData(Qt.UserRole, False) # to safely ignore them later + plugin_name = f"{plugin_name} (Core Plugin)" + item.setText(plugin_name) + + items.append(item) + + return items diff --git a/profile_manager/profile_manager.py b/profile_manager/profile_manager.py index f461a29..7429fc4 100644 --- a/profile_manager/profile_manager.py +++ b/profile_manager/profile_manager.py @@ -1,213 +1,127 @@ -""" -/*************************************************************************** - ProfileManager - A QGIS plugin - Makes creating profiles easy by giving you an UI to easly import settings from other profiles - Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ - ------------------- - begin : 2020-03-17 - git sha : $Format:%H$ - copyright : (C) 2020 by Stefan Giese & Dominik Szill / WhereGroup GmbH - email : stefan.giese@wheregroup.com / dominik.szill@wheregroup.com - ***************************************************************************/ - -/*************************************************************************** - * * - * 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 the code for the dialog +import logging import time -from collections import defaultdict -from os import path from pathlib import Path from shutil import copytree +from typing import Optional -# PyQGIS from qgis.core import Qgis, QgsMessageLog, QgsUserProfileManager from qgis.PyQt.QtCore import ( QCoreApplication, QLocale, QSettings, - QSize, - Qt, QTranslator, ) from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QAction, QMessageBox, QWidget +from qgis.PyQt.QtWidgets import QAction, QWidget -# plugin -from profile_manager.datasources.dataservices.datasource_handler import ( - DataSourceHandler, +from profile_manager.datasources.bookmarks import import_bookmarks +from profile_manager.datasources.customization import ( + import_customizations, ) -from profile_manager.gui.interface_handler import InterfaceHandler +from profile_manager.datasources.data_sources import ( + collect_data_sources, + import_data_sources, + remove_data_sources, +) +from profile_manager.datasources.favourites import import_favourites +from profile_manager.datasources.expressions import ( + import_expressions, +) +from profile_manager.datasources.models import import_models +from profile_manager.datasources.scripts import import_scripts +from profile_manager.datasources.plugins import ( + collect_plugin_names, + remove_plugins, + import_plugins, +) +from profile_manager.datasources.styles import import_styles + from profile_manager.profile_manager_dialog import ProfileManagerDialog -from profile_manager.profiles.profile_action_handler import ProfileActionHandler +from profile_manager.profiles.profile_handler import ( + create_profile, + copy_profile, + rename_profile, + remove_profile, +) from profile_manager.profiles.utils import get_profile_qgis_ini_path, qgis_profiles_path -from profile_manager.utils import adjust_to_operating_system, wait_cursor +from profile_manager.utils import wait_cursor + + +LOGGER = logging.getLogger("profile_manager") +logging.basicConfig( + format="%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s", + datefmt="%H:%M:%S", + level=logging.DEBUG, +) class ProfileManager: """QGIS Plugin Implementation.""" def __init__(self, iface): - """Constructor. + self.backup_path = Path.home() / "QGIS Profile Manager Backup" - :param iface: An interface instance that will be passed to this class - which provides the hook by which you can manipulate the QGIS - application at run time. - :type iface: QgsInterface - """ - # Classwide vars - self.is_cancel_button_clicked = False - self.is_ok_button_clicked = False - self.backup_path = "" - self.qgis_profiles_path = "" - self.ini_path = "" - self.operating_system = "" - self.qgs_profile_manager = ( - None # TODO in QGIS 3.30 we could and should use iface.userProfileManager() - ) - self.data_source_handler: DataSourceHandler = None - self.profile_manager_action_handler: ProfileActionHandler = None - self.interface_handler: InterfaceHandler = None - self.dlg = None + # TODO in QGIS 3.30 we could and should use iface.userProfileManager() + self.qgs_profile_manager = None + + self.__dlg: Optional[ProfileManagerDialog] = None # Save reference to the QGIS interface self.iface = iface # initialize plugin directory - self.plugin_dir = path.dirname(__file__) - # initialize locale + self.__plugin_dir = Path(__file__).parent.absolute() + # initialize locale locale = QSettings().value("locale/userLocale", QLocale().name())[0:2] - locale_path = path.join( - self.plugin_dir, "i18n", "ProfileManager_{}.qm".format(locale) - ) - - if path.exists(locale_path): - self.translator = QTranslator() - self.translator.load(locale_path) - QCoreApplication.installTranslator(self.translator) + locale_path = self.__plugin_dir / "i18n" / "ProfileManager_{}.qm".format(locale) + if locale_path.exists(): + self.__translator = QTranslator() + self.__translator.load(str(locale_path)) + QCoreApplication.installTranslator(self.__translator) # Declare instance attributes - self.actions = [] + self.action: Optional[QAction] = None self.menu = self.tr("&Profile Manager") # Check if plugin was started the first time in current QGIS session # Must be set in initGui() to survive plugin reloads self.first_start = None - # noinspection PyMethodMayBeStatic - def tr(self, message): - """Get the translation for a string using Qt translation API. - - We implement this ourselves since we do not inherit QObject. - - :param message: String for translation. - :type message: str, QString - - :returns: Translated version of message. - :rtype: QString - """ - # noinspection PyTypeChecker,PyArgumentList,PyCallByClass - return QCoreApplication.translate("ProfileManager", message) - - def add_action( - self, - icon_path, - text, - callback, - enabled_flag=True, - add_to_menu=True, - add_to_toolbar=True, - status_tip=None, - whats_this=None, - parent=None, - ): - """Add a toolbar icon to the toolbar. - - :param icon_path: Path to the icon for this action. Can be a resource - path (e.g. ':/plugins/foo/bar.png') or a normal file system path. - :type icon_path: str - - :param text: Text that should be shown in menu items for this action. - :type text: str - - :param callback: Function to be called when the action is triggered. - :type callback: function - - :param enabled_flag: A flag indicating if the action should be enabled - by default. Defaults to True. - :type enabled_flag: bool - - :param add_to_menu: Flag indicating whether the action should also - be added to the menu. Defaults to True. - :type add_to_menu: bool - - :param add_to_toolbar: Flag indicating whether the action should also - be added to the toolbar. Defaults to True. - :type add_to_toolbar: bool - - :param status_tip: Optional text to show in a popup when mouse pointer - hovers over the action. - :type status_tip: str - - :param parent: Parent widget for the new action. Defaults None. - :type parent: QWidget - - :param whats_this: Optional text to show in the status bar when the - mouse pointer hovers over the action. - - :returns: The action that was created. Note that the action is also - added to self.actions list. - :rtype: QAction - """ - - icon = QIcon(icon_path) - action = QAction(icon, text, parent) - action.triggered.connect(callback) - action.setEnabled(enabled_flag) + self.source_profile_name: Optional[str] = None # e.g. "My Profile + self.source_profile_path: Optional[Path] = None # e.g. ".../My Profile" + self.source_qgis_ini_file: Optional[Path] = None # e.g. ".../QGIS3.ini" + self.source_data_sources: Optional[dict] = None + self.source_plugins: Optional[list] = None - if status_tip is not None: - action.setStatusTip(status_tip) + self.target_profile_name: Optional[str] = None # e.g. "My Other Profile + self.target_profile_path: Optional[Path] = None # e.g. ".../My Other Profile" + self.target_qgis_ini_file: Optional[Path] = None # e.g. ".../QGIS3.ini" + self.target_data_sources: Optional[dict] = None + self.target_plugins: Optional[list] = None - if whats_this is not None: - action.setWhatsThis(whats_this) - - if add_to_toolbar: - # Adds plugin icon to Plugins toolbar - self.iface.addToolBarIcon(action) - - if add_to_menu: - self.iface.addPluginToMenu(self.menu, action) - - self.actions.append(action) - - return action + def __refresh_qgis_browser_panels(self): + """Refreshes the browser of the qgis instance from which this plugin was started""" + self.iface.mainWindow().findChildren(QWidget, "Browser")[0].refresh() + self.iface.mainWindow().findChildren(QWidget, "Browser2")[0].refresh() def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" - self.add_action( - path.join(path.dirname(__file__), "icon.png"), - text=self.tr("Profile Manager"), - callback=self.run, - parent=self.iface.mainWindow(), - ) + icon = QIcon(str(self.__plugin_dir / "icon.png")) + action = QAction(icon, self.tr("Profile Manager"), self.iface.mainWindow()) + action.triggered.connect(self.run) + action.setEnabled(True) + self.iface.addToolBarIcon(action) + self.iface.addPluginToMenu(self.menu, action) + self.action = action # will be set False in run() self.first_start = True def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" - for action in self.actions: - self.iface.removePluginMenu(self.tr("&Profile Manager"), action) - self.iface.removeToolBarIcon(action) + self.iface.removePluginMenu(self.menu, self.action) + self.iface.removeToolBarIcon(self.action) def run(self): """Run method that performs all the real work""" @@ -216,258 +130,298 @@ def run(self): with wait_cursor(): if self.first_start: self.first_start = False - self.dlg = ProfileManagerDialog(parent=self.iface.mainWindow()) - self.dlg.setFixedSize(self.dlg.size()) - self.dlg.list_profiles.setIconSize(QSize(15, 15)) - - self.set_paths() - - self.qgs_profile_manager = QgsUserProfileManager( - self.qgis_profiles_path + self.__dlg = ProfileManagerDialog( + profile_manager=self, parent=self.iface.mainWindow() ) - self.data_source_handler = DataSourceHandler(self.dlg, self) - self.profile_manager_action_handler = ProfileActionHandler( - self.dlg, self.qgis_profiles_path, self + self.qgs_profile_manager = QgsUserProfileManager( + str(qgis_profiles_path()) ) - self.interface_handler = InterfaceHandler(self, self.dlg) - self.interface_handler.setup_connections() + # on any start: + self.__dlg.populate_profile_listings() + # data sources and plugins are populated via signals - self.interface_handler.populate_profile_listings() - self.interface_handler.populate_data_source_tree( - self.dlg.comboBoxNamesSource.currentText(), True - ) - self.interface_handler.populate_data_source_tree( - self.dlg.comboBoxNamesTarget.currentText(), False - ) - - self.data_source_handler.set_path_to_files( - self.dlg.comboBoxNamesSource.currentText(), - self.dlg.comboBoxNamesTarget.currentText(), - ) - self.data_source_handler.display_plugins() + self.__dlg.exec() - self.dlg.exec() - - def set_paths(self): - """Sets various OS and profile dependent paths""" - self.qgis_profiles_path = str(qgis_profiles_path()) - - self.backup_path = adjust_to_operating_system( - str(Path.home()) + "/QGIS Profile Manager Backup/" - ) + # noinspection PyMethodMayBeStatic + def tr(self, message): + return QCoreApplication.translate("ProfileManager", message) - def make_backup(self, profile: str): + def change_source_profile(self, profile_name: str): + # TODO handle profile_name=None without any attempts of data collecting + self.source_profile_name = profile_name + self.source_profile_path = qgis_profiles_path() / profile_name + self.source_qgis_ini_file = get_profile_qgis_ini_path(profile_name) + self.source_data_sources = collect_data_sources(self.source_qgis_ini_file) + self.source_plugins = collect_plugin_names(self.source_qgis_ini_file) + + def change_target_profile(self, profile_name: str): + # TODO handle profile_name=None without any attempts of data collecting + self.target_profile_name = profile_name + self.target_profile_path = qgis_profiles_path() / profile_name + self.target_qgis_ini_file = get_profile_qgis_ini_path(profile_name) + self.target_data_sources = collect_data_sources(self.target_qgis_ini_file) + self.target_plugins = collect_plugin_names(self.target_qgis_ini_file) + + def make_backup(self, profile_name: str) -> Optional[str]: """Creates a backup of the specified profile. Args: - profile (str): Name of the profile to back up + profile_name (str): Name of the profile to back up - Raises: - OSError: If copytree raises something + Returns: + str: A message if an error occured. """ ts = int(time.time()) - target_path = self.backup_path + str(ts) - source_path = f"{self.qgis_profiles_path}/{profile}" + target_path = self.backup_path / str(ts) + source_path = qgis_profiles_path() / profile_name QgsMessageLog.logMessage( - f"Backing up profile '{source_path}' to '{target_path}'", + f"Backing up profile {profile_name!r} to {target_path!r}", "Profile Manager", - level=Qgis.Info, + level=Qgis.MessageLevel.Info, ) - copytree(source_path, target_path) + try: + copytree(source_path, target_path) + except Exception as e: + return self.tr("Error while creating backup: {}").format(e) + + def create_profile(self, profile_name: str) -> Optional[str]: + try: + create_profile(profile_name) + except Exception as e: + return self.tr( + "Creation of profile '{0}' failed due to error:\n{1}" + ).format(profile_name, e) + + def copy_profile( + self, source_profile_name: str, target_profile_name: str + ) -> Optional[str]: + try: + copy_profile(source_profile_name, target_profile_name) + except Exception as e: + return self.tr( + "Copying of profile '{0}' to '{1}' failed due to error:\n{2}" + ).format(source_profile_name, target_profile_name, e) + + def rename_profile( + self, old_profile_name: str, new_profile_name: str + ) -> Optional[str]: + try: + rename_profile(old_profile_name, new_profile_name) + except Exception as e: + return self.tr( + "Renaming of profile '{0}' failed due to error:\n{1}" + ).format(old_profile_name, e) + + def remove_profile(self, profile_name: str) -> Optional[str]: + try: + remove_profile(profile_name) + except Exception as e: + return self.tr("Removal of profile '{0}' failed due to error:\n{1}").format( + profile_name, e + ) - def import_action_handler(self): - """Handles data source import + def import_things( + self, + data_sources: dict[str, list[str]], + plugins: list[str], + do_import_bookmarks: bool, + do_import_favourites: bool, + do_import_models: bool, + do_import_scripts: bool, + do_import_styles: bool, + do_import_expressions: bool, + do_import_customizations: bool, + ) -> list[str]: + """Handles import of all things supported.""" + # safety catch, should be prevented by the GUI + if self.source_profile_name == self.target_profile_name: + return [self.tr("Cannot import things from profile into itself")] + if not self.target_profile_name: + return [self.tr("No target profile selected")] + + error_messages = [] + if data_sources: + QgsMessageLog.logMessage( + self.tr("Importing {} data sources...").format( + sum([len(v) for v in data_sources.values()]) + ), + "Profile Manager", + level=Qgis.MessageLevel.Info, + ) + try: + import_data_sources( + qgis_ini_file=self.target_qgis_ini_file, + data_sources_to_be_imported=data_sources, + available_data_sources=self.source_data_sources, + ) + except Exception as e: + error_messages.append( + self.tr("Error while importing data sources: {}").format(e) + ) + self.target_data_sources = collect_data_sources(self.target_qgis_ini_file) - Aborts and shows an error message if no backup could be made. - """ - error_message = None - with wait_cursor(): - self.get_checked_sources() - source_profile_name = self.dlg.comboBoxNamesSource.currentText() - target_profile_name = self.dlg.comboBoxNamesTarget.currentText() - assert ( - source_profile_name != target_profile_name - ) # should be forced by the GUI - self.data_source_handler.set_path_to_files( - source_profile_name, target_profile_name + if plugins: + QgsMessageLog.logMessage( + self.tr("Importing {} plugins...").format(len(plugins)), + "Profile Manager", + level=Qgis.MessageLevel.Info, ) - self.data_source_handler.set_path_to_bookmark_files( - source_profile_name, target_profile_name + try: + import_plugins( + self.source_profile_path, + self.target_profile_path, + self.target_qgis_ini_file, + plugins, + ) + except Exception as e: + error_messages.append( + self.tr("Error while importing plugins: {}").format(e) + ) + self.target_plugins = collect_plugin_names(self.target_qgis_ini_file) + + if do_import_bookmarks: + QgsMessageLog.logMessage( + self.tr("Importing bookmarks..."), + "Profile Manager", + level=Qgis.MessageLevel.Info, ) try: - self.make_backup(target_profile_name) - except OSError as e: - error_message = self.tr("Aborting import due to error:\n{}").format(e) + import_bookmarks( + self.source_profile_path / "bookmarks.xml", + self.target_profile_path / "bookmarks.xml", + ) + except Exception as e: + error_messages.append( + self.tr("Error while importing bookmarks: {}").format(e) + ) - if error_message: - QMessageBox.critical( - None, self.tr("Backup could not be created"), error_message + if do_import_favourites: + QgsMessageLog.logMessage( + self.tr("Importing favourites..."), + "Profile Manager", + level=Qgis.MessageLevel.Info, + ) + try: + import_favourites(self.source_qgis_ini_file, self.target_qgis_ini_file) + except Exception as e: + error_messages.append( + self.tr("Error while importing favourites: {}").format(e) ) - return - with wait_cursor(): - self.data_source_handler.import_plugins() - errors_on_sources = self.data_source_handler.import_all_the_things() - self.update_data_sources(only_update_plugins_for_target_profile=True) - - if errors_on_sources: - QMessageBox.critical( - None, - self.tr("Data Source Import"), - self.tr( - "There were errors on import." - ), # The user should have been shown dialogs or see a log + if do_import_models: + QgsMessageLog.logMessage( + self.tr("Importing models..."), + "Profile Manager", + level=Qgis.MessageLevel.Info, ) - else: - QMessageBox.information( - None, - self.tr("Data Source Import"), - self.tr( - "Data sources have been successfully imported.\n\n" - "Please refresh the QGIS Browser to see the changes." - ), + try: + import_models(self.source_profile_path, self.target_profile_path) + except Exception as e: + error_messages.append( + self.tr("Error while importing models: {}").format(e) + ) + + if do_import_scripts: + QgsMessageLog.logMessage( + self.tr("Importing scripts..."), + "Profile Manager", + level=Qgis.MessageLevel.Info, ) - self.interface_handler.uncheck_everything() - self.refresh_browser_model() + try: + import_scripts(self.source_profile_path, self.target_profile_path) + except Exception as e: + error_messages.append( + self.tr("Error while importing scripts: {}").format(e) + ) - def remove_source_action_handler(self): - """Handles data source removal + if do_import_styles: + QgsMessageLog.logMessage( + self.tr("Importing styles..."), + "Profile Manager", + level=Qgis.MessageLevel.Info, + ) + try: + import_styles(self.source_profile_path, self.target_profile_path) + except Exception as e: + error_messages.append( + self.tr("Error while importing styles: {}").format(e) + ) - Aborts and shows an error message if no backup could be made. - """ - self.get_checked_sources() - source_profile_name = self.dlg.comboBoxNamesSource.currentText() - self.data_source_handler.set_path_to_files(source_profile_name, "") - - clicked_button = QMessageBox.question( - None, - self.tr("Remove Data Sources"), - self.tr( - "Are you sure you want to remove these sources?\n\nA backup will be created at '{}'" - ).format(self.backup_path), - ) + if do_import_expressions: + QgsMessageLog.logMessage( + self.tr("Importing expressions..."), + "Profile Manager", + level=Qgis.MessageLevel.Info, + ) + try: + import_expressions(self.source_qgis_ini_file, self.target_qgis_ini_file) + except Exception as e: + error_messages.append( + self.tr("Error while importing expressions: {}").format(e) + ) - if clicked_button == QMessageBox.Yes: - error_message = None - with wait_cursor(): - try: - self.make_backup(source_profile_name) - except OSError as e: - error_message = self.tr( - "Aborting removal due to error:\n{}" - ).format(e) - - if not error_message: - self.data_source_handler.remove_datasources_and_plugins() - self.update_data_sources(True) - - if error_message: - QMessageBox.critical( - None, self.tr("Backup could not be created"), error_message + if do_import_customizations: + QgsMessageLog.logMessage( + self.tr("Importing customizations..."), + "Profile Manager", + level=Qgis.MessageLevel.Info, + ) + try: + import_customizations( + self.source_profile_path, self.target_profile_path ) - else: - QMessageBox.information( - None, - self.tr("Data Sources Removed"), - self.tr( - "Data sources have been successfully removed.\n\n" - "Please refresh the QGIS Browser to see the changes." - ), + except Exception as e: + error_messages.append( + self.tr("Error while importing UI customizations: {}").format(e) ) - self.refresh_browser_model() - self.interface_handler.uncheck_everything() - - def update_data_sources( - self, only_update_plugins_for_target_profile=False, update_source=True - ): - """Updates data sources and plugin lists in the UI""" - source_profile = self.dlg.comboBoxNamesSource.currentText() - target_profile = self.dlg.comboBoxNamesTarget.currentText() - - if update_source: - self.interface_handler.populate_data_source_tree(source_profile, True) - self.interface_handler.populate_data_source_tree(target_profile, False) - else: - self.interface_handler.populate_data_source_tree(target_profile, False) - - self.data_source_handler.display_plugins( - only_for_target_profile=only_update_plugins_for_target_profile - ) - def get_checked_sources(self): - """Gets all checked data sources and communicates them to the data source handler""" - - # TODO why is the split between web and db necessary?? - # TODO what titles does QGIS use in the GUI? can we use the same when needed in the plugin? - - checked_web_sources = defaultdict(list) - checked_database_sources = defaultdict(list) - - for item in self.dlg.treeWidgetSource.findItems( - "", Qt.MatchContains | Qt.MatchRecursive - ): - if item.childCount() == 0 and item.checkState(0) == Qt.Checked: - parent_text = item.parent().text(0) # the provider group in the tree - item_text = item.text( - 0 - ) # a specific data source in the provider's group - # FIXME hardcoded list of GUI titles - if parent_text in [ - "SpatiaLite", - "PostgreSQL", - "MSSQL", - "DB2", - "Oracle", - ]: - checked_database_sources[parent_text].append(item_text) - # GeoPackage connections are stored under [providers] in the ini - elif ( - parent_text == "GeoPackage" - ): # FIXME hardcoded relationship between GeoPackage and 'providers' - checked_database_sources["providers"].append(item_text) - else: - checked_web_sources[parent_text].append(item_text) - - self.data_source_handler.set_data_sources( - checked_web_sources, checked_database_sources - ) - - def get_profile_paths(self) -> tuple[str, str]: - """Returns the paths to the currently chosen source and target profiles. + self.__refresh_qgis_browser_panels() - Returns: - tuple[str, str]: Path to source profile, path to target profile - """ - source = adjust_to_operating_system( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesSource.currentText() - + "/" - ) - target = adjust_to_operating_system( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesTarget.currentText() - + "/" - ) + def remove_things( + self, data_sources: dict[str, list[str]], plugins: list[str] + ) -> list[str]: + """Handles removal of data sources and plugins. Other things are not supported (yet).""" + error_messages = [] - return source, target + if data_sources: + QgsMessageLog.logMessage( + self.tr("Removing {} data sources...").format( + sum([len(v) for v in data_sources.values()]) + ), + "Profile Manager", + level=Qgis.MessageLevel.Info, + ) + try: + remove_data_sources( + qgis_ini_file=self.source_qgis_ini_file, + data_sources_to_be_removed=data_sources, + available_data_sources=self.source_data_sources, + ) + except Exception as e: + error_messages.append( + self.tr("Error while removing data sources: {}").format(e) + ) + self.source_data_sources = collect_data_sources(self.source_qgis_ini_file) - def get_ini_paths(self): - """Gets path to current chosen source and target qgis.ini file""" - ini_paths = { - "source": str( - get_profile_qgis_ini_path(self.dlg.comboBoxNamesSource.currentText()) - ), - "target": str( - get_profile_qgis_ini_path(self.dlg.comboBoxNamesTarget.currentText()) - ), - } + if plugins: + QgsMessageLog.logMessage( + self.tr("Removing {} plugins...").format(len(plugins)), + "Profile Manager", + level=Qgis.MessageLevel.Info, + ) + try: + remove_plugins( + self.source_profile_path, + self.source_qgis_ini_file, + plugins, + ) + except Exception as e: + error_messages.append( + self.tr("Error while removing plugins: {}").format(e) + ) + self.source_plugins = collect_plugin_names(self.source_qgis_ini_file) - return ini_paths + self.__refresh_qgis_browser_panels() - def refresh_browser_model(self): - """Refreshes the browser of the qgis instance from which this plugin was started""" - self.iface.mainWindow().findChildren(QWidget, "Browser")[0].refresh() - self.iface.mainWindow().findChildren(QWidget, "Browser2")[0].refresh() + return error_messages diff --git a/profile_manager/profile_manager_dialog.py b/profile_manager/profile_manager_dialog.py index afc68ae..e418859 100644 --- a/profile_manager/profile_manager_dialog.py +++ b/profile_manager/profile_manager_dialog.py @@ -1,52 +1,38 @@ -""" -/*************************************************************************** - ProfileManagerDialog - A QGIS plugin - Makes creating profiles easy by giving you an UI to easly import settings from other profiles - Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ - ------------------- - begin : 2020-03-17 - git sha : $Format:%H$ - copyright : (C) 2020 by Dominik Szill / WhereGroup GmbH - email : dominik.szill@wheregroup.com - ***************************************************************************/ - -/*************************************************************************** - * * - * 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. * - * * - ***************************************************************************/ -""" - -# standard -import os +from collections import defaultdict from pathlib import Path -from typing import Optional +from typing import Optional, Literal -# pyQGIS from qgis.PyQt import QtWidgets, uic -from qgis.PyQt.QtWidgets import QMessageBox +from qgis.PyQt.QtCore import QSize, Qt +from qgis.PyQt.QtWidgets import ( + QDialog, + QListWidget, + QMessageBox, + QTreeWidget, +) + +from qgis.core import QgsApplication -# plugin from profile_manager.gui.mdl_profiles import ProfileListModel +from profile_manager.gui.name_profile_dialog import NameProfileDialog +from profile_manager.gui.utils import ( + data_sources_as_tree, + plugins_as_items, +) from profile_manager.qdt_export.profile_export import ( QDTProfileInfos, export_profile_for_qdt, get_qdt_profile_infos_from_file, ) +from profile_manager.utils import wait_cursor -# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer FORM_CLASS, _ = uic.loadUiType( - os.path.join(os.path.dirname(__file__), "profile_manager_dialog_base.ui") + Path(__file__).parent.absolute() / "profile_manager_dialog_base.ui" ) class ProfileManagerDialog(QtWidgets.QDialog, FORM_CLASS): - def __init__(self, parent=None): - """Constructor.""" + def __init__(self, profile_manager, parent=None): super().__init__(parent) # Set up the user interface from Designer through FORM_CLASS. # After self.setupUi() you can access any designer object by doing @@ -55,6 +41,9 @@ def __init__(self, parent=None): # #widgets-and-dialogs-with-auto-connect self.setupUi(self) + self.__profile_manager = profile_manager + self.__everything_is_checked = False + self.profile_mdl = ProfileListModel(self) self.qdt_export_profile_cbx.setModel(self.profile_mdl) self.export_qdt_button.clicked.connect(self.export_qdt_handler) @@ -64,6 +53,45 @@ def __init__(self, parent=None): self.comboBoxNamesSource.setModel(self.profile_mdl) self.comboBoxNamesTarget.setModel(self.profile_mdl) self.list_profiles.setModel(self.profile_mdl) + self.setFixedSize(self.size()) + self.list_profiles.setIconSize(QSize(15, 15)) + + self.__setup_connections() + + # initial population of things on import tab + self.comboBoxNamesSource.currentTextChanged.emit( + self.comboBoxNamesSource.currentText() + ) + self.comboBoxNamesTarget.currentTextChanged.emit( + self.comboBoxNamesTarget.currentText() + ) + + def __setup_connections(self): + """Set up connections""" + # buttons + self.importThingsButton.clicked.connect(self.__import_selected_things) + self.removeThingsButton.clicked.connect(self.__remove_selected_things) + + self.createProfileButton.clicked.connect(self.__create_profile) + self.removeProfileButton.clicked.connect(self.__remove_profile) + self.editProfileButton.clicked.connect(self.__rename_profile) + self.copyProfileButton.clicked.connect(self.__copy_profile) + + self.closeButton.rejected.connect(self.reject) + + # checkbox + self.checkBox_checkAll.stateChanged.connect(self.__toggle_all_items) + + # selections/indexes + self.comboBoxNamesSource.currentTextChanged.connect( + self.__on_source_profile_changed + ) + self.comboBoxNamesTarget.currentTextChanged.connect( + self.__on_target_profile_changed + ) + self.list_profiles.selectionModel().selectionChanged.connect( + self.__conditionally_enable_profile_buttons + ) def get_list_selection_profile_name(self) -> Optional[str]: """Get selected profile name from list @@ -135,3 +163,505 @@ def export_qdt_handler(self) -> None: self.tr("QDT profile export"), self.tr("QDT profile have been successfully exported."), ) + + def __conditionally_enable_import_buttons(self): + source = self.__profile_manager.source_profile_name + target = self.__profile_manager.target_profile_name + if source == target: + self.removeThingsButton.setEnabled(True) + self.importThingsButton.setEnabled(False) + elif source is None and target is not None: + self.removeThingsButton.setEnabled(False) + self.importThingsButton.setEnabled(False) + elif source is not None and target is None: + self.importThingsButton.setEnabled(False) + self.removeThingsButton.setEnabled(True) + else: + self.importThingsButton.setEnabled(True) + self.removeThingsButton.setEnabled(True) + + def __conditionally_enable_profile_buttons(self): + """Sets up buttons of the Profiles tab so that the user is not tempted to do "impossible" things. + + Called when profile selection changes in the Profiles tab. + """ + # A profile must be selected + if self.get_list_selection_profile_name() is None: + self.removeProfileButton.setToolTip( + self.tr("Please choose a profile to remove") + ) + self.removeProfileButton.setEnabled(False) + self.editProfileButton.setToolTip( + self.tr("Please choose a profile to rename") + ) + self.editProfileButton.setEnabled(False) + self.copyProfileButton.setToolTip( + self.tr("Please select a profile to copy from") + ) + self.copyProfileButton.setEnabled(False) + # Some actions can/should not be done on the currently active profile + elif ( + self.get_list_selection_profile_name() + == Path(QgsApplication.qgisSettingsDirPath()).name + ): + self.removeProfileButton.setToolTip( + self.tr("The active profile cannot be removed") + ) + self.removeProfileButton.setEnabled(False) + self.editProfileButton.setToolTip( + self.tr("The active profile cannot be renamed") + ) + self.editProfileButton.setEnabled(False) + self.copyProfileButton.setToolTip("") + self.copyProfileButton.setEnabled(True) + else: + self.removeProfileButton.setToolTip("") + self.removeProfileButton.setEnabled(True) + self.editProfileButton.setToolTip("") + self.editProfileButton.setEnabled(True) + self.copyProfileButton.setToolTip("") + self.copyProfileButton.setEnabled(True) + + def __on_source_profile_changed(self, profile_name: str): + self.__profile_manager.change_source_profile(profile_name) + self.__conditionally_enable_import_buttons() + + if profile_name is None: + self.treeWidgetSource.clear() + self.list_plugins_source.clear() + else: + self.__update_data_sources_widget( + "source", self.__profile_manager.source_data_sources + ) + self.__update_plugins_widget( + "source", self.__profile_manager.source_plugins + ) + + def __on_target_profile_changed(self, profile_name: str): + self.__profile_manager.change_target_profile(profile_name) + self.__conditionally_enable_import_buttons() + + if profile_name is None: + self.treeWidgetTarget.clear() + self.list_plugins_target.clear() + else: + self.__update_data_sources_widget( + "target", self.__profile_manager.target_data_sources + ) + self.__update_plugins_widget( + "target", self.__profile_manager.target_plugins + ) + + def populate_profile_listings(self): + """Populates the main list as well as the comboboxes with available profile names. + + Also updates button states according to resulting selections. + + TODO this docstring seems not correct anymore. + TODO how//where IS the profile model updated? + TODO document WHY blocksignals is used + """ + self.comboBoxNamesSource.blockSignals(True) + active_profile_name = Path(QgsApplication.qgisSettingsDirPath()).name + self.comboBoxNamesSource.setCurrentText(active_profile_name) + self.comboBoxNamesSource.blockSignals(False) + + self.__conditionally_enable_profile_buttons() + + def __populate_data_sources( + self, + data_sources: dict, + data_sources_widget: QTreeWidget, + make_checkable=True, + ): + """Populates the specified widget with a fancy list of available data sources. + + Args: + data_sources: TODO + data_sources_widget: The widget to populate + make_checkable: If the data source items should be checkable by the user + """ + # create tree items for discovered data sources, grouped by provider + data_source_list = [] + for provider, provider_data_sources in data_sources.items(): + if not provider_data_sources: + continue + tree_root_item = data_sources_as_tree( + provider, provider_data_sources, make_checkable=make_checkable + ) + data_source_list.append(tree_root_item) + + # populate tree + data_sources_widget.clear() + for tree_root_item in data_source_list: + data_sources_widget.addTopLevelItem(tree_root_item) + + def __populate_plugins_list( + self, plugins: list[str], plugins_widget: QListWidget, make_checkable: bool + ): + """Populates the specified widget with a fancy list of available plugins. + + Args: + plugins: Names of plugins + plugins_widget: The widget to populate + make_checkable: If the plugin items should be checkable by the user + """ + items = plugins_as_items(plugins, make_checkable) + + plugins_widget.clear() + for item in items: + plugins_widget.addItem(item) + + def __set_checkstates(self, checkstate: Qt.CheckState): + for item in self.treeWidgetSource.findItems( + "", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive + ): + item.setCheckState(0, checkstate) + + for item in self.list_plugins.findItems( + "", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive + ): + item.setCheckState(checkstate) + + self.bookmark_check.setCheckState(checkstate) + self.favourites_check.setCheckState(checkstate) + self.models_check.setCheckState(checkstate) + self.scripts_check.setCheckState(checkstate) + self.styles_check.setCheckState(checkstate) + self.expressions_check.setCheckState(checkstate) + self.checkBox_checkAll.setCheckState(checkstate) + self.customization_check.setCheckState(checkstate) + + def __toggle_all_items(self): + """Checks/Unchecks every checkbox in the gui""" + if self.__everything_is_checked: + checkstate = Qt.CheckState.Unchecked + else: + checkstate = Qt.CheckState.Checked + self.__set_checkstates(checkstate) + self.__everything_is_checked = not self.__everything_is_checked + + def __uncheck_everything(self): + """Unchecks every checkbox""" + self.__set_checkstates(Qt.CheckState.Unchecked) + self.__everything_is_checked = False + + def __update_data_sources_widget( + self, profile_to_update: Literal["source", "target"], data_sources: dict + ): + """Updates data sources and plugin lists in the UI""" + if profile_to_update == "source": + self.__populate_data_sources( + data_sources=data_sources, + data_sources_widget=self.treeWidgetSource, + make_checkable=True, + ) + elif profile_to_update == "target": + self.__populate_data_sources( + data_sources=data_sources, + data_sources_widget=self.treeWidgetTarget, + make_checkable=False, + ) + else: + raise ValueError("Only source or target profile can be updated") + + def __update_plugins_widget( + self, profile_to_update: Literal["source", "target"], plugins: list[str] + ): + if profile_to_update == "source": + self.__populate_plugins_list( + plugins=plugins, + plugins_widget=self.list_plugins, + make_checkable=True, + ) + elif profile_to_update == "target": + self.__populate_plugins_list( + plugins=plugins, + plugins_widget=self.list_plugins_target, + make_checkable=False, + ) + else: + raise ValueError("Only source or target profile can be updated") + + def __create_profile(self): + """Creates a new profile""" + name_dialog = NameProfileDialog() + if name_dialog.exec() == QDialog.Rejected: + return + profile_name = name_dialog.text_input.text() + + with wait_cursor(): + error_message = self.__profile_manager.create_profile(profile_name) + + if error_message: + QMessageBox.critical( + self, self.tr("Profile could not be created"), error_message + ) + else: + QMessageBox.information( + self, + self.tr("Profile created"), + self.tr("Profile '{}' successfully created.").format(profile_name), + ) + + self.populate_profile_listings() + + def __copy_profile(self): + """Copies the selected profile""" + source_profile_name = self.get_list_selection_profile_name() + + name_dialog = NameProfileDialog( + title=self.tr("Name for copy of profile '{}'").format(source_profile_name) + ) + if name_dialog.exec() == QDialog.Rejected: + return + target_profile_name = name_dialog.text_input.text() + + with wait_cursor(): + error_message = self.__profile_manager.copy_profile( + source_profile_name, target_profile_name + ) + + if error_message: + QMessageBox.critical( + self, + self.tr("Profile '{0}' could not be copied to '{1}'").format( + source_profile_name, target_profile_name + ), + error_message, + ) + else: + QMessageBox.information( + self, + self.tr("Profile copied"), + self.tr("Profile '{0}' successfully copied to '{1}'.").format( + source_profile_name, target_profile_name + ), + ) + + self.populate_profile_listings() + + def __rename_profile(self): + """Renames the selected profile""" + old_profile_name = self.get_list_selection_profile_name() + + name_dialog = NameProfileDialog() + if name_dialog.exec() == QDialog.Rejected: + return + new_profile_name = name_dialog.text_input.text() + + with wait_cursor(): + error_message = self.__profile_manager.rename_profile( + old_profile_name, new_profile_name + ) + if error_message: + QMessageBox.critical( + self, self.tr("Profile could not be renamed"), error_message + ) + else: + QMessageBox.information( + self, + self.tr("Profile renamed"), + self.tr("Profile '{0}' successfully renamed to '{1}'.").format( + old_profile_name, new_profile_name + ), + ) + + self.populate_profile_listings() + + def __remove_profile(self): + """Removes the selected profile (after creating a backup).""" + profile_name = self.get_list_selection_profile_name() + + do_remove_profile = QMessageBox.question( + self, + self.tr("Remove Profile"), + self.tr( + "Are you sure you want to remove the profile '{0}'?\n\nA backup will be created at '{1}".format( + profile_name, self.__profile_manager.backup_path + ) + ), + ) + if do_remove_profile == QMessageBox.No: + return + + with wait_cursor(): + error_message = self.__profile_manager.make_backup(profile_name) + if error_message: + QMessageBox.critical( + self, + self.tr("Backup could not be created"), + self.tr("Aborting removal of profile '{0}' due to error:\n{1}").format( + profile_name, error_message + ), + ) + return + + with wait_cursor(): + error_message = self.__profile_manager.remove_profile(profile_name) + if error_message: + QMessageBox.critical( + self, self.tr("Profile could not be removed"), error_message + ) + else: + QMessageBox.information( + self, + self.tr("Profile removed"), + self.tr("Profile '{}' has been removed.").format(profile_name), + ) + + self.populate_profile_listings() + + def __selected_data_sources(self) -> dict[str, list[str]]: + """Returns all data sources selected by the user in the source profile. + + Returns: + provider -> [data source 1, data source 2, ...] + """ + checked_data_sources = defaultdict(list) + + for item in self.treeWidgetSource.findItems( + "", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive + ): + if item.childCount() == 0 and item.checkState(0) == Qt.CheckState.Checked: + # the provider group in the tree + parent_text = item.parent().text(0) + + # a specific data source in the provider's group + item_text = item.text(0) + + checked_data_sources[parent_text].append(item_text) + + return checked_data_sources + + def __selected_plugins(self) -> list[str]: + """Returns all plugins (names) selected by the user in the source profile.""" + plugin_names = [] + for item in self.list_plugins.findItems( + "", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive + ): + if item.data(Qt.UserRole) is False: # Core Plugins are marked with this + continue + if item.checkState() == Qt.CheckState.Checked: + plugin_names.append(item.text()) + return plugin_names + + def __import_selected_things(self): + """Import selected things from the source to the target profile. + + Aborts and shows an error message if no backup could be made. + """ + + with wait_cursor(): + error_message = self.__profile_manager.make_backup( + self.__profile_manager.target_profile_name + ) + if error_message: + QMessageBox.critical( + self, + self.tr("Backup could not be created"), + self.tr("Aborting import due to error:\n{}").format(error_message), + ) + return + + with wait_cursor(): + selected_data_sources = self.__selected_data_sources() + selected_plugins = self.__selected_plugins() + error_messages = self.__profile_manager.import_things( + data_sources=selected_data_sources, + plugins=selected_plugins, + do_import_bookmarks=self.bookmark_check.isChecked(), + do_import_favourites=self.favourites_check.isChecked(), + do_import_models=self.models_check.isChecked(), + do_import_scripts=self.scripts_check.isChecked(), + do_import_styles=self.styles_check.isChecked(), + do_import_expressions=self.expressions_check.isChecked(), + do_import_customizations=self.customization_check.isChecked(), + ) + + if error_messages: + QMessageBox.critical( + self, self.tr("Import Error(s)"), "\n".join(error_messages) + ) + else: + QMessageBox.information( + self, + self.tr("Import"), + self.tr("Selected items have been successfully imported."), + ) + + with wait_cursor(): + if selected_data_sources: + self.__update_data_sources_widget( + "target", self.__profile_manager.target_data_sources + ) + if selected_plugins: + self.__update_plugins_widget( + "target", self.__profile_manager.target_plugins + ) + self.__uncheck_everything() + + def __remove_selected_things(self): + """Removes selected things from the source profile. + + Aborts and shows an error message if no backup could be made. + """ + + do_remove_things = QMessageBox.question( + self, + self.tr("Removal"), + self.tr( + ( + "Are you sure you want to remove the selected data sources and plugins?" + "\n\nA backup will be created at {}" + ) + ).format(self.__profile_manager.backup_path), + ) + if do_remove_things == QMessageBox.No: + return + + with wait_cursor(): + error_message = self.__profile_manager.make_backup( + self.__profile_manager.source_profile_name + ) + if error_message: + QMessageBox.critical( + self, + self.tr("Backup could not be created"), + self.tr( + "Aborting removal of selected data sources and plugins due to error:\n{}" + ).format(error_message), + ) + return + + with wait_cursor(): + selected_data_sources = self.__selected_data_sources() + selected_plugins = self.__selected_plugins() + error_messages = self.__profile_manager.remove_things( + data_sources=selected_data_sources, plugins=selected_plugins + ) + if error_messages: + QMessageBox.critical( + self, + self.tr("Removal Error(s)"), + "\n".join(error_messages), + ) + else: + QMessageBox.information( + self, + self.tr("Removal"), + self.tr( + "Selected data sources and plugins have been successfully removed." + ), + ) + + with wait_cursor(): + if selected_data_sources: + self.__update_data_sources_widget( + "source", self.__profile_manager.source_data_sources + ) + if selected_plugins: + self.__update_plugins_widget( + "source", self.__profile_manager.source_plugins + ) + self.__uncheck_everything() diff --git a/profile_manager/profile_manager_dialog_base.ui b/profile_manager/profile_manager_dialog_base.ui index 8e50990..c83e1a0 100644 --- a/profile_manager/profile_manager_dialog_base.ui +++ b/profile_manager/profile_manager_dialog_base.ui @@ -148,7 +148,7 @@ - + Remove selected items from source profile @@ -158,7 +158,7 @@ - + Import selected items from source to target profile @@ -204,7 +204,7 @@ - Source Profile: + Data sources in source profile @@ -219,7 +219,7 @@ - Target Profile: + Data sources in target profile @@ -310,16 +310,16 @@ - + UI Customization (e.g. hidden toolbar icons) - + - Expression Functions + Expressions @@ -469,7 +469,7 @@ - + QDialogButtonBox::Close diff --git a/profile_manager/profiles/profile_action_handler.py b/profile_manager/profiles/profile_action_handler.py deleted file mode 100644 index 23d4d8e..0000000 --- a/profile_manager/profiles/profile_action_handler.py +++ /dev/null @@ -1,48 +0,0 @@ -from qgis.PyQt.QtWidgets import QDialog - -from profile_manager.profiles.profile_copier import ProfileCopier -from profile_manager.profiles.profile_creator import ProfileCreator -from profile_manager.profiles.profile_editor import ProfileEditor -from profile_manager.profiles.profile_remover import ProfileRemover - - -class ProfileActionHandler(QDialog): - - def __init__( - self, profile_manager_dialog, qgis_path, profile_manager, *args, **kwargs - ): - super().__init__(*args, **kwargs) - - self.is_cancel_button_clicked = False - self.is_ok_button_clicked = False - self.dlg = profile_manager_dialog - self.qgis_path = qgis_path - self.profile_manager = profile_manager - self.profile_remover = ProfileRemover( - self.dlg, self.qgis_path, self.profile_manager - ) - self.profile_creator = ProfileCreator(self.qgis_path, self.profile_manager) - self.profile_editor = ProfileEditor( - self.dlg, self.qgis_path, self.profile_manager - ) - self.profile_copier = ProfileCopier(self.dlg, self.qgis_path) - - def create_new_profile(self): - """Creates a new profile""" - self.profile_creator.create_new_profile() - self.profile_manager.interface_handler.populate_profile_listings() - - def copy_profile(self): - """Copies the selected profile""" - self.profile_copier.copy_profile() - self.profile_manager.interface_handler.populate_profile_listings() - - def edit_profile(self): - """Edits the selected profile""" - self.profile_editor.edit_profile() - self.profile_manager.interface_handler.populate_profile_listings() - - def remove_profile(self): - """Removes the selected profile""" - self.profile_remover.remove_profile() - self.profile_manager.interface_handler.populate_profile_listings() diff --git a/profile_manager/profiles/profile_copier.py b/profile_manager/profiles/profile_copier.py deleted file mode 100644 index b5e037c..0000000 --- a/profile_manager/profiles/profile_copier.py +++ /dev/null @@ -1,45 +0,0 @@ -from shutil import copytree - -from qgis.PyQt.QtWidgets import QDialog, QMessageBox - -from profile_manager.gui.name_profile_dialog import NameProfileDialog -from profile_manager.utils import wait_cursor - - -class ProfileCopier(QDialog): - - def __init__(self, profile_manager_dialog, qgis_path, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.dlg = profile_manager_dialog - self.qgis_path = qgis_path - - def copy_profile(self): - source_profile = self.dlg.get_list_selection_profile_name() - assert source_profile is not None # should be forced by the GUI - source_profile_path = self.qgis_path + "/" + source_profile + "/" - - dialog = NameProfileDialog() - return_code = dialog.exec() - if return_code == QDialog.Accepted: - error_message = None - with wait_cursor(): - profile_name = dialog.text_input.text() - assert profile_name != "" # should be forced by the GUI - profile_path = self.qgis_path + "/" + profile_name + "/" - try: - copytree(source_profile_path, profile_path) - except FileExistsError: - error_message = self.tr( - "Profile directory '{}' already exists." - ).format(profile_name) - if error_message: - QMessageBox.critical( - None, self.tr("Profile could not be copied"), error_message - ) - else: - QMessageBox.information( - None, - self.tr("Profile copied"), - self.tr("Profile '{}' successfully copied.").format(profile_name), - ) diff --git a/profile_manager/profiles/profile_creator.py b/profile_manager/profiles/profile_creator.py deleted file mode 100644 index 5dfa3e6..0000000 --- a/profile_manager/profiles/profile_creator.py +++ /dev/null @@ -1,58 +0,0 @@ -from os import mkdir -from sys import platform - -from qgis.core import QgsUserProfileManager -from qgis.PyQt.QtWidgets import QDialog, QMessageBox - -from profile_manager.gui.name_profile_dialog import NameProfileDialog -from profile_manager.utils import adjust_to_operating_system, wait_cursor - - -class ProfileCreator(QDialog): - - def __init__(self, qgis_path, profile_manager, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.profile_manager = profile_manager - self.qgis_path = qgis_path - self.qgs_profile_manager = QgsUserProfileManager(self.qgis_path) - - def create_new_profile(self): - """Creates new profile with user inputs name""" - dialog = NameProfileDialog() - return_code = dialog.exec() - if return_code == QDialog.Accepted: - error_message = None - with wait_cursor(): - profile_name = dialog.text_input.text() - assert profile_name != "" # should be forced by the GUI - self.qgs_profile_manager.createUserProfile(profile_name) - try: - if platform == "darwin": - profile_path = ( - self.qgis_path + "/" + profile_name + "/qgis.org/" - ) - else: - profile_path = self.qgis_path + "/" + profile_name + "/QGIS/" - - profile_path = adjust_to_operating_system(profile_path) - mkdir(profile_path) - - ini_path = profile_path + adjust_to_operating_system("QGIS3.ini") - qgis_ini_file = open(ini_path, "w") - qgis_ini_file.close() - except FileExistsError: - error_message = self.tr( - "Profile directory '{}' already exists." - ).format(profile_name) - - if error_message: - QMessageBox.critical( - None, self.tr("Profile could not be created"), error_message - ) - else: - QMessageBox.information( - None, - self.tr("Profile created"), - self.tr("Profile '{}' successfully created.").format(profile_name), - ) diff --git a/profile_manager/profiles/profile_editor.py b/profile_manager/profiles/profile_editor.py deleted file mode 100644 index 1bbe061..0000000 --- a/profile_manager/profiles/profile_editor.py +++ /dev/null @@ -1,63 +0,0 @@ -from os import rename -from pathlib import Path - -from qgis.core import QgsApplication -from qgis.PyQt.QtWidgets import QDialog, QMessageBox - -from profile_manager.gui.name_profile_dialog import NameProfileDialog -from profile_manager.utils import adjust_to_operating_system, wait_cursor - - -class ProfileEditor(QDialog): - - def __init__( - self, profile_manager_dialog, qgis_path, profile_manager, *args, **kwargs - ): - super().__init__(*args, **kwargs) - - self.dlg = profile_manager_dialog - self.profile_manager = profile_manager - self.qgis_path = qgis_path - - def edit_profile(self): - """Renames profile with user input""" - old_profile_name = self.dlg.get_list_selection_profile_name() - # bad states that should be prevented by the GUI - assert old_profile_name is not None - assert old_profile_name != Path(QgsApplication.qgisSettingsDirPath()).name - - profile_before_change = adjust_to_operating_system( - self.qgis_path + "/" + old_profile_name - ) - - dialog = NameProfileDialog( - title=self.tr("Rename Profile '{}'").format(old_profile_name) - ) - return_code = dialog.exec() - - if return_code == QDialog.Accepted: - error_message = None - with wait_cursor(): - new_profile_name = dialog.text_input.text() - assert new_profile_name != "" # should be forced by the GUI - profile_after_change = adjust_to_operating_system( - self.qgis_path + "/" + new_profile_name - ) - - try: - rename(profile_before_change, profile_after_change) - except OSError as e: - error_message = str(e) - - if error_message: - QMessageBox.critical( - None, self.tr("Profile could not be renamed"), error_message - ) - else: - QMessageBox.information( - None, - self.tr("Profile renamed"), - self.tr("Profile '{0}' successfully renamed to '{1}'.").format( - old_profile_name, new_profile_name - ), - ) diff --git a/profile_manager/profiles/profile_handler.py b/profile_manager/profiles/profile_handler.py new file mode 100644 index 0000000..d5c9e9c --- /dev/null +++ b/profile_manager/profiles/profile_handler.py @@ -0,0 +1,85 @@ +import re + +from os import rename +from pathlib import Path +from shutil import rmtree, copytree +from sys import platform + +from qgis.core import QgsUserProfileManager, QgsApplication + +from profile_manager.profiles.utils import qgis_profiles_path + +# validation rule from QGIS' QgsUserProfileSelectionDialog +VALID_PROJECT_NAME_REGEX = "[^/\\\\]+" + + +def create_profile(profile_name: str): + """Creates new profile""" + if not profile_name: + raise ValueError("Empty profile name provided") + if not re.match(VALID_PROJECT_NAME_REGEX, profile_name): + raise ValueError("Invalid profile name") + + qgs_profile_manager = QgsUserProfileManager(str(qgis_profiles_path())) + qgs_profile_manager.createUserProfile(profile_name) + + # Right now there is only the profile directory and the qgis.db in its root. + # We want to be able to write things to the profile's QGIS3.ini file so: + if platform == "darwin": + sub_dir = "qgis.org" + else: + sub_dir = "QGIS" + ini_dir_path = qgis_profiles_path() / profile_name / sub_dir + ini_dir_path.mkdir() + ini_path = ini_dir_path / "QGIS3.ini" + ini_path.touch() + + +def remove_profile(profile_name: str): + """Removes profile""" + if not profile_name: + raise ValueError("Empty profile name provided") + if not re.match(VALID_PROJECT_NAME_REGEX, profile_name): + raise ValueError("Invalid profile name") + if profile_name == Path(QgsApplication.qgisSettingsDirPath()).name: + raise ValueError("Cannot remove the profile that is currently active") + + profile_path = qgis_profiles_path() / profile_name + rmtree(profile_path) + + +def copy_profile(source_profile_name: str, target_profile_name: str): + if not source_profile_name: + raise ValueError("Empty source profile name provided") + if not target_profile_name: + raise ValueError("Empty target profile name provided") + if not re.match(VALID_PROJECT_NAME_REGEX, source_profile_name): + raise ValueError("Invalid source profile name") + if not re.match(VALID_PROJECT_NAME_REGEX, target_profile_name): + raise ValueError("Invalid target profile name") + if source_profile_name == target_profile_name: + raise ValueError("Cannot copy profile to itself") + + source_profile_path = qgis_profiles_path() / source_profile_name + profile_path = qgis_profiles_path() / target_profile_name + + copytree(source_profile_path, profile_path) + + +def rename_profile(old_profile_name: str, new_profile_name: str): + """Renames profile to new name.""" + if not old_profile_name: + raise ValueError("Empty old profile name provided") + if not old_profile_name: + raise ValueError("Empty new profile name provided") + if not re.match(VALID_PROJECT_NAME_REGEX, old_profile_name): + raise ValueError("Invalid old profile name") + if not re.match(VALID_PROJECT_NAME_REGEX, new_profile_name): + raise ValueError("Invalid new profile name") + if old_profile_name == Path(QgsApplication.qgisSettingsDirPath()).name: + raise ValueError("Cannot rename the profile that is currently active") + + profile_before_change = qgis_profiles_path() / old_profile_name + profile_after_change = qgis_profiles_path() / new_profile_name + + rename(profile_before_change, profile_after_change) diff --git a/profile_manager/profiles/profile_remover.py b/profile_manager/profiles/profile_remover.py deleted file mode 100644 index eec47f1..0000000 --- a/profile_manager/profiles/profile_remover.py +++ /dev/null @@ -1,74 +0,0 @@ -from pathlib import Path -from shutil import rmtree - -from qgis.core import QgsApplication -from qgis.PyQt.QtWidgets import QDialog, QMessageBox - -from profile_manager.utils import adjust_to_operating_system, wait_cursor - - -class ProfileRemover(QDialog): - - def __init__( - self, profile_manager_dialog, qgis_path, profile_manager, *args, **kwargs - ): - super().__init__(*args, **kwargs) - - self.dlg = profile_manager_dialog - self.profile_manager = profile_manager - self.qgis_path = qgis_path - - def remove_profile(self): - """Removes profile - - Aborts and shows an error message if no backup could be made. - """ - profile_name = self.dlg.get_list_selection_profile_name() - # bad states that should be prevented by the GUI - assert profile_name is not None - assert profile_name != Path(QgsApplication.qgisSettingsDirPath()).name - - profile_path = adjust_to_operating_system(self.qgis_path + "/" + profile_name) - - clicked_button = QMessageBox.question( - None, - self.tr("Remove Profile"), - self.tr( - "Are you sure you want to remove the profile '{0}'?\n\nA backup will be created at '{1}'" - ).format(profile_name, self.profile_manager.backup_path), - ) - - if clicked_button == QMessageBox.Yes: - error_message = None - - with wait_cursor(): - try: - self.profile_manager.make_backup(profile_name) - except OSError as e: - error_message = self.tr( - "Aborting removal of profile '{0}' due to error:\n{1}" - ).format(profile_name, e) - if error_message: - QMessageBox.critical( - None, self.tr("Backup could not be created"), error_message - ) - return - - with wait_cursor(): - try: - rmtree(profile_path) - except FileNotFoundError as e: - error_message = self.tr( - "Aborting removal of profile '{0}' due to error:\n{1}" - ).format(profile_name, e) - - if error_message: - QMessageBox.critical( - None, self.tr("Profile could not be removed"), error_message - ) - else: - QMessageBox.information( - None, - self.tr("Profile removed"), - self.tr("Profile '{}' has been removed.").format(profile_name), - ) diff --git a/profile_manager/utils.py b/profile_manager/utils.py index 6e821cb..4966016 100644 --- a/profile_manager/utils.py +++ b/profile_manager/utils.py @@ -1,5 +1,4 @@ from contextlib import contextmanager -from sys import platform from qgis.PyQt.QtCore import QCoreApplication, Qt from qgis.PyQt.QtGui import QCursor, QGuiApplication @@ -14,23 +13,6 @@ def wait_cursor(): QGuiApplication.restoreOverrideCursor() -def adjust_to_operating_system(path_to_adjust): - """Adjusts path to current OS. - - For MacOS it contains special logic to also replace the /QGIS/ -> /qgis.org/ directory name. - """ - if platform.startswith("win32"): - return path_to_adjust.replace("/", "\\") - elif platform.startswith("linux") or "bsd" in platform: - return path_to_adjust.replace("\\", "/") - elif platform.startswith("darwin"): # macos - return path_to_adjust.replace("\\", "/").replace( - "/QGIS/QGIS3.ini", "/qgis.org/QGIS3.ini" - ) - else: - raise NotImplementedError(f"Unsupported platform '{platform}'") - - def tr(message): # for translating in non-QObject class contexts return QCoreApplication.translate("ProfileManager", message) From a0269f520896a247264df72fb966ff85b29e6f8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 21:56:48 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- profile_manager/datasources/data_sources.py | 2 -- profile_manager/datasources/plugins.py | 4 +--- profile_manager/profile_manager.py | 25 ++++++--------------- profile_manager/profile_manager_dialog.py | 17 ++++---------- profile_manager/profiles/profile_handler.py | 5 ++--- 5 files changed, 14 insertions(+), 39 deletions(-) diff --git a/profile_manager/datasources/data_sources.py b/profile_manager/datasources/data_sources.py index 3d520d3..08a2318 100644 --- a/profile_manager/datasources/data_sources.py +++ b/profile_manager/datasources/data_sources.py @@ -1,5 +1,4 @@ import logging - from configparser import RawConfigParser from dataclasses import dataclass from datetime import datetime @@ -7,7 +6,6 @@ from re import compile from urllib.parse import unquote - LOGGER = logging.getLogger("profile_manager") diff --git a/profile_manager/datasources/plugins.py b/profile_manager/datasources/plugins.py index 4c8bdbe..6979da3 100644 --- a/profile_manager/datasources/plugins.py +++ b/profile_manager/datasources/plugins.py @@ -1,10 +1,8 @@ import logging - from configparser import NoSectionError, RawConfigParser from datetime import datetime from pathlib import Path -from shutil import rmtree, copytree - +from shutil import copytree, rmtree LOGGER = logging.getLogger("profile_manager") diff --git a/profile_manager/profile_manager.py b/profile_manager/profile_manager.py index 7429fc4..4bc22be 100644 --- a/profile_manager/profile_manager.py +++ b/profile_manager/profile_manager.py @@ -5,48 +5,37 @@ from typing import Optional from qgis.core import Qgis, QgsMessageLog, QgsUserProfileManager -from qgis.PyQt.QtCore import ( - QCoreApplication, - QLocale, - QSettings, - QTranslator, -) +from qgis.PyQt.QtCore import QCoreApplication, QLocale, QSettings, QTranslator from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QWidget from profile_manager.datasources.bookmarks import import_bookmarks -from profile_manager.datasources.customization import ( - import_customizations, -) +from profile_manager.datasources.customization import import_customizations from profile_manager.datasources.data_sources import ( collect_data_sources, import_data_sources, remove_data_sources, ) +from profile_manager.datasources.expressions import import_expressions from profile_manager.datasources.favourites import import_favourites -from profile_manager.datasources.expressions import ( - import_expressions, -) from profile_manager.datasources.models import import_models -from profile_manager.datasources.scripts import import_scripts from profile_manager.datasources.plugins import ( collect_plugin_names, - remove_plugins, import_plugins, + remove_plugins, ) +from profile_manager.datasources.scripts import import_scripts from profile_manager.datasources.styles import import_styles - from profile_manager.profile_manager_dialog import ProfileManagerDialog from profile_manager.profiles.profile_handler import ( - create_profile, copy_profile, - rename_profile, + create_profile, remove_profile, + rename_profile, ) from profile_manager.profiles.utils import get_profile_qgis_ini_path, qgis_profiles_path from profile_manager.utils import wait_cursor - LOGGER = logging.getLogger("profile_manager") logging.basicConfig( format="%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s", diff --git a/profile_manager/profile_manager_dialog.py b/profile_manager/profile_manager_dialog.py index e418859..2e0ff7a 100644 --- a/profile_manager/profile_manager_dialog.py +++ b/profile_manager/profile_manager_dialog.py @@ -1,24 +1,15 @@ from collections import defaultdict from pathlib import Path -from typing import Optional, Literal +from typing import Literal, Optional +from qgis.core import QgsApplication from qgis.PyQt import QtWidgets, uic from qgis.PyQt.QtCore import QSize, Qt -from qgis.PyQt.QtWidgets import ( - QDialog, - QListWidget, - QMessageBox, - QTreeWidget, -) - -from qgis.core import QgsApplication +from qgis.PyQt.QtWidgets import QDialog, QListWidget, QMessageBox, QTreeWidget from profile_manager.gui.mdl_profiles import ProfileListModel from profile_manager.gui.name_profile_dialog import NameProfileDialog -from profile_manager.gui.utils import ( - data_sources_as_tree, - plugins_as_items, -) +from profile_manager.gui.utils import data_sources_as_tree, plugins_as_items from profile_manager.qdt_export.profile_export import ( QDTProfileInfos, export_profile_for_qdt, diff --git a/profile_manager/profiles/profile_handler.py b/profile_manager/profiles/profile_handler.py index d5c9e9c..fae2b5f 100644 --- a/profile_manager/profiles/profile_handler.py +++ b/profile_manager/profiles/profile_handler.py @@ -1,11 +1,10 @@ import re - from os import rename from pathlib import Path -from shutil import rmtree, copytree +from shutil import copytree, rmtree from sys import platform -from qgis.core import QgsUserProfileManager, QgsApplication +from qgis.core import QgsApplication, QgsUserProfileManager from profile_manager.profiles.utils import qgis_profiles_path From 7187ab5b5b19b0c3ab0359668f0e7a95fdd033df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Kr=C3=B6ger?= Date: Wed, 5 Feb 2025 12:10:29 +0100 Subject: [PATCH 3/6] Rename "datasources" directory to "handlers" --- profile_manager/gui/utils.py | 2 +- .../{datasources => handlers}/__init__.py | 0 .../{datasources => handlers}/bookmarks.py | 0 .../{datasources => handlers}/customization.py | 0 .../{datasources => handlers}/data_sources.py | 0 .../{datasources => handlers}/expressions.py | 0 .../{datasources => handlers}/favourites.py | 0 .../{datasources => handlers}/models.py | 0 .../{datasources => handlers}/plugins.py | 0 .../{datasources => handlers}/scripts.py | 0 .../{datasources => handlers}/styles.py | 0 profile_manager/profile_manager.py | 18 +++++++++--------- 12 files changed, 10 insertions(+), 10 deletions(-) rename profile_manager/{datasources => handlers}/__init__.py (100%) rename profile_manager/{datasources => handlers}/bookmarks.py (100%) rename profile_manager/{datasources => handlers}/customization.py (100%) rename profile_manager/{datasources => handlers}/data_sources.py (100%) rename profile_manager/{datasources => handlers}/expressions.py (100%) rename profile_manager/{datasources => handlers}/favourites.py (100%) rename profile_manager/{datasources => handlers}/models.py (100%) rename profile_manager/{datasources => handlers}/plugins.py (100%) rename profile_manager/{datasources => handlers}/scripts.py (100%) rename profile_manager/{datasources => handlers}/styles.py (100%) diff --git a/profile_manager/gui/utils.py b/profile_manager/gui/utils.py index 862696b..f61a317 100644 --- a/profile_manager/gui/utils.py +++ b/profile_manager/gui/utils.py @@ -1,7 +1,7 @@ from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtWidgets import QListWidgetItem, QTreeWidgetItem -from profile_manager.datasources.plugins import CORE_PLUGINS +from profile_manager.handlers.plugins import CORE_PLUGINS def data_sources_as_tree( diff --git a/profile_manager/datasources/__init__.py b/profile_manager/handlers/__init__.py similarity index 100% rename from profile_manager/datasources/__init__.py rename to profile_manager/handlers/__init__.py diff --git a/profile_manager/datasources/bookmarks.py b/profile_manager/handlers/bookmarks.py similarity index 100% rename from profile_manager/datasources/bookmarks.py rename to profile_manager/handlers/bookmarks.py diff --git a/profile_manager/datasources/customization.py b/profile_manager/handlers/customization.py similarity index 100% rename from profile_manager/datasources/customization.py rename to profile_manager/handlers/customization.py diff --git a/profile_manager/datasources/data_sources.py b/profile_manager/handlers/data_sources.py similarity index 100% rename from profile_manager/datasources/data_sources.py rename to profile_manager/handlers/data_sources.py diff --git a/profile_manager/datasources/expressions.py b/profile_manager/handlers/expressions.py similarity index 100% rename from profile_manager/datasources/expressions.py rename to profile_manager/handlers/expressions.py diff --git a/profile_manager/datasources/favourites.py b/profile_manager/handlers/favourites.py similarity index 100% rename from profile_manager/datasources/favourites.py rename to profile_manager/handlers/favourites.py diff --git a/profile_manager/datasources/models.py b/profile_manager/handlers/models.py similarity index 100% rename from profile_manager/datasources/models.py rename to profile_manager/handlers/models.py diff --git a/profile_manager/datasources/plugins.py b/profile_manager/handlers/plugins.py similarity index 100% rename from profile_manager/datasources/plugins.py rename to profile_manager/handlers/plugins.py diff --git a/profile_manager/datasources/scripts.py b/profile_manager/handlers/scripts.py similarity index 100% rename from profile_manager/datasources/scripts.py rename to profile_manager/handlers/scripts.py diff --git a/profile_manager/datasources/styles.py b/profile_manager/handlers/styles.py similarity index 100% rename from profile_manager/datasources/styles.py rename to profile_manager/handlers/styles.py diff --git a/profile_manager/profile_manager.py b/profile_manager/profile_manager.py index 4bc22be..0cdb168 100644 --- a/profile_manager/profile_manager.py +++ b/profile_manager/profile_manager.py @@ -9,23 +9,23 @@ from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QWidget -from profile_manager.datasources.bookmarks import import_bookmarks -from profile_manager.datasources.customization import import_customizations -from profile_manager.datasources.data_sources import ( +from profile_manager.handlers.bookmarks import import_bookmarks +from profile_manager.handlers.customization import import_customizations +from profile_manager.handlers.data_sources import ( collect_data_sources, import_data_sources, remove_data_sources, ) -from profile_manager.datasources.expressions import import_expressions -from profile_manager.datasources.favourites import import_favourites -from profile_manager.datasources.models import import_models -from profile_manager.datasources.plugins import ( +from profile_manager.handlers.expressions import import_expressions +from profile_manager.handlers.favourites import import_favourites +from profile_manager.handlers.models import import_models +from profile_manager.handlers.plugins import ( collect_plugin_names, import_plugins, remove_plugins, ) -from profile_manager.datasources.scripts import import_scripts -from profile_manager.datasources.styles import import_styles +from profile_manager.handlers.scripts import import_scripts +from profile_manager.handlers.styles import import_styles from profile_manager.profile_manager_dialog import ProfileManagerDialog from profile_manager.profiles.profile_handler import ( copy_profile, From 161b59a627de883fdef2ca3480115c83f5168f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Kr=C3=B6ger?= Date: Wed, 5 Feb 2025 13:19:23 +0100 Subject: [PATCH 4/6] More Qt6 enums --- profile_manager/gui/mdl_profiles.py | 10 +++++++--- profile_manager/gui/name_profile_dialog.py | 4 ++-- profile_manager/gui/utils.py | 2 +- profile_manager/profile_manager_dialog.py | 14 ++++++++------ profile_manager/utils.py | 2 +- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/profile_manager/gui/mdl_profiles.py b/profile_manager/gui/mdl_profiles.py index 13a43ea..c12f23f 100644 --- a/profile_manager/gui/mdl_profiles.py +++ b/profile_manager/gui/mdl_profiles.py @@ -42,7 +42,7 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlags: Qt.ItemFlags: flags """ default_flags = super().flags(index) - return default_flags & ~Qt.ItemIsEditable # Disable editing + return default_flags & ~Qt.ItemFlags.ItemIsEditable # Disable editing def _update_available_profiles(self) -> None: """Update model with all available profiles in manager""" @@ -63,7 +63,9 @@ def insert_profile(self, profile_name: str) -> None: self.insertRow(row) self.setData(self.index(row, self.NAME_COL), profile.name()) self.setData( - self.index(row, self.NAME_COL), profile.icon(), Qt.DecorationRole + self.index(row, self.NAME_COL), + profile.icon(), + Qt.ItemDataRole.DecorationRole, ) active_profile_folder_name = Path(QgsApplication.qgisSettingsDirPath()).name @@ -71,4 +73,6 @@ def insert_profile(self, profile_name: str) -> None: if profile_folder_name == active_profile_folder_name: font = QgsApplication.font() font.setItalic(True) - self.setData(self.index(row, self.NAME_COL), font, Qt.FontRole) + self.setData( + self.index(row, self.NAME_COL), font, Qt.ItemDataRole.FontRole + ) diff --git a/profile_manager/gui/name_profile_dialog.py b/profile_manager/gui/name_profile_dialog.py index 06f904e..99dddab 100644 --- a/profile_manager/gui/name_profile_dialog.py +++ b/profile_manager/gui/name_profile_dialog.py @@ -26,7 +26,7 @@ def __init__(self, title=None, *args, **kwargs): QRegularExpressionValidator(QRegularExpression("[^/\\\\]+")) ) - self.button_box = QDialogButtonBox.Ok | QDialogButtonBox.Cancel + self.button_box = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel self.button_box = QDialogButtonBox(self.button_box) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) @@ -42,7 +42,7 @@ def __init__(self, title=None, *args, **kwargs): def adjust_ok_button_state(self): """Disable OK button if no profile name has been entered (yet)""" - ok_button = self.button_box.button(QDialogButtonBox.Ok) + ok_button = self.button_box.button(QDialogButtonBox.StandardButton.Ok) if self.text_input.text() == "": ok_button.setEnabled(False) else: diff --git a/profile_manager/gui/utils.py b/profile_manager/gui/utils.py index f61a317..a696b2b 100644 --- a/profile_manager/gui/utils.py +++ b/profile_manager/gui/utils.py @@ -59,7 +59,7 @@ def plugins_as_items(plugins: list[str], make_checkable: bool) -> list[QListWidg if plugin_name in CORE_PLUGINS: item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEnabled) - item.setData(Qt.UserRole, False) # to safely ignore them later + item.setData(Qt.ItemDataRole.UserRole, False) # to safely ignore them later plugin_name = f"{plugin_name} (Core Plugin)" item.setText(plugin_name) diff --git a/profile_manager/profile_manager_dialog.py b/profile_manager/profile_manager_dialog.py index 2e0ff7a..669bb93 100644 --- a/profile_manager/profile_manager_dialog.py +++ b/profile_manager/profile_manager_dialog.py @@ -377,7 +377,7 @@ def __update_plugins_widget( def __create_profile(self): """Creates a new profile""" name_dialog = NameProfileDialog() - if name_dialog.exec() == QDialog.Rejected: + if name_dialog.exec() == QDialog.DialogCode.Rejected: return profile_name = name_dialog.text_input.text() @@ -404,7 +404,7 @@ def __copy_profile(self): name_dialog = NameProfileDialog( title=self.tr("Name for copy of profile '{}'").format(source_profile_name) ) - if name_dialog.exec() == QDialog.Rejected: + if name_dialog.exec() == QDialog.DialogCode.Rejected: return target_profile_name = name_dialog.text_input.text() @@ -437,7 +437,7 @@ def __rename_profile(self): old_profile_name = self.get_list_selection_profile_name() name_dialog = NameProfileDialog() - if name_dialog.exec() == QDialog.Rejected: + if name_dialog.exec() == QDialog.DialogCode.Rejected: return new_profile_name = name_dialog.text_input.text() @@ -473,7 +473,7 @@ def __remove_profile(self): ) ), ) - if do_remove_profile == QMessageBox.No: + if do_remove_profile == QMessageBox.StandardButton.No: return with wait_cursor(): @@ -531,7 +531,9 @@ def __selected_plugins(self) -> list[str]: for item in self.list_plugins.findItems( "", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive ): - if item.data(Qt.UserRole) is False: # Core Plugins are marked with this + if ( + item.data(Qt.ItemDataRole.UserRole) is False + ): # Core Plugins are marked with this continue if item.checkState() == Qt.CheckState.Checked: plugin_names.append(item.text()) @@ -608,7 +610,7 @@ def __remove_selected_things(self): ) ).format(self.__profile_manager.backup_path), ) - if do_remove_things == QMessageBox.No: + if do_remove_things == QMessageBox.StandardButton.No: return with wait_cursor(): diff --git a/profile_manager/utils.py b/profile_manager/utils.py index 4966016..87907f9 100644 --- a/profile_manager/utils.py +++ b/profile_manager/utils.py @@ -7,7 +7,7 @@ @contextmanager def wait_cursor(): try: - QGuiApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + QGuiApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) yield finally: QGuiApplication.restoreOverrideCursor() From 6b3ba61c44490da0c10d5504abb068386edaa8a0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:21:07 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- profile_manager/gui/name_profile_dialog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/profile_manager/gui/name_profile_dialog.py b/profile_manager/gui/name_profile_dialog.py index 99dddab..09aaba6 100644 --- a/profile_manager/gui/name_profile_dialog.py +++ b/profile_manager/gui/name_profile_dialog.py @@ -26,7 +26,9 @@ def __init__(self, title=None, *args, **kwargs): QRegularExpressionValidator(QRegularExpression("[^/\\\\]+")) ) - self.button_box = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + self.button_box = ( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) self.button_box = QDialogButtonBox(self.button_box) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) From 24545a594d8b2a32746282b406e4ef93329facf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Kr=C3=B6ger?= Date: Wed, 5 Feb 2025 14:33:59 +0100 Subject: [PATCH 6/6] Update metadata.txt to 0.6.0 --- profile_manager/metadata.txt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/profile_manager/metadata.txt b/profile_manager/metadata.txt index a0715ba..95976a2 100644 --- a/profile_manager/metadata.txt +++ b/profile_manager/metadata.txt @@ -22,5 +22,20 @@ qgisMaximumVersion=3.99 server=False # versioning -version=0.5.0-beta2 +version=0.6.0 changelog= + Version 0.6.0: + - Another huge refactor, many bug fixes + Version 0.5: + - Support for exporting profiles for QGIS Deployment Toolbelt (QDT) + Version 0.4: + - Fairly big refactoring and cleanup + - Better and more verbose error handling + - Improve performance + - Reduce backup size, change backup directory + - Improve dialogs and messages + - Add support for Vector Tiles connections + - Fix a crash (thanks Ivano Giuliano!) + - ... + Version 0.3: + - ... Tales of prehistoric times ...