diff --git a/CHANGELOG.md b/CHANGELOG.md index a3cca0c..9248df6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Unreleased ## 0.5.0-beta1 - 2024-10-04 +- add tab to export profile for QGIS Deployment Toolbelt - add modern plugin's packaging using QGIS Plugin CI - apply Python coding rules to whole codebase (PEP8) - remove dead code diff --git a/profile_manager/gui/interface_handler.py b/profile_manager/gui/interface_handler.py index e83b594..934be6d 100644 --- a/profile_manager/gui/interface_handler.py +++ b/profile_manager/gui/interface_handler.py @@ -1,9 +1,8 @@ from pathlib import Path from qgis.core import Qgis, QgsApplication, QgsMessageLog -from qgis.PyQt.QtCore import Qt, QVariant -from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QDialog, QListWidgetItem +from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtWidgets import QDialog from profile_manager.datasources.dataservices.datasource_provider import ( DATA_SOURCE_SEARCH_LOCATIONS, @@ -73,36 +72,8 @@ def populate_profile_listings(self): Also updates button states according to resulting selections. """ - profile_names = self.profile_manager.qgs_profile_manager.allProfiles() - active_profile_name = Path(QgsApplication.qgisSettingsDirPath()).name - self.dlg.comboBoxNamesSource.blockSignals(True) - self.dlg.comboBoxNamesTarget.blockSignals(True) - self.dlg.list_profiles.blockSignals(True) - - self.dlg.comboBoxNamesSource.clear() - self.dlg.comboBoxNamesTarget.clear() - self.dlg.list_profiles.clear() - for i, name in enumerate(profile_names): - # Init source profiles combobox - self.dlg.comboBoxNamesSource.addItem(name) - if name == active_profile_name: - font = self.dlg.comboBoxNamesSource.font() - font.setItalic(True) - self.dlg.comboBoxNamesSource.setItemData(i, QVariant(font), Qt.FontRole) - # Init target profiles combobox - self.dlg.comboBoxNamesTarget.addItem(name) - if name == active_profile_name: - font = self.dlg.comboBoxNamesTarget.font() - font.setItalic(True) - self.dlg.comboBoxNamesTarget.setItemData(i, QVariant(font), Qt.FontRole) - # Add profiles to list view - list_item = QListWidgetItem(QIcon("../icon.png"), name) - if name == active_profile_name: - font = list_item.font() - font.setItalic(True) - list_item.setFont(font) - self.dlg.list_profiles.addItem(list_item) + active_profile_name = Path(QgsApplication.qgisSettingsDirPath()).name self.dlg.comboBoxNamesSource.setCurrentText(active_profile_name) @@ -150,7 +121,7 @@ def setup_connections(self): self.dlg.comboBoxNamesTarget.currentIndexChanged.connect( self.conditionally_enable_import_button ) - self.dlg.list_profiles.currentItemChanged.connect( + self.dlg.list_profiles.selectionModel().selectionChanged.connect( self.conditionally_enable_profile_buttons ) @@ -223,7 +194,7 @@ def conditionally_enable_profile_buttons(self): Called when profile selection changes in the Profiles tab. """ # A profile must be selected - if self.dlg.list_profiles.currentItem() is None: + if self.dlg.get_list_selection_profile_name() is None: self.dlg.removeProfileButton.setToolTip( self.tr("Please choose a profile to remove") ) @@ -238,7 +209,7 @@ def conditionally_enable_profile_buttons(self): self.dlg.copyProfileButton.setEnabled(False) # Some actions can/should not be done on the currently active profile elif ( - self.dlg.list_profiles.currentItem().text() + self.dlg.get_list_selection_profile_name() == Path(QgsApplication.qgisSettingsDirPath()).name ): self.dlg.removeProfileButton.setToolTip( diff --git a/profile_manager/gui/mdl_profiles.py b/profile_manager/gui/mdl_profiles.py new file mode 100644 index 0000000..13a43ea --- /dev/null +++ b/profile_manager/gui/mdl_profiles.py @@ -0,0 +1,74 @@ +from pathlib import Path + +from qgis.core import QgsApplication, QgsUserProfile, QgsUserProfileManager +from qgis.PyQt.QtCore import QModelIndex, QObject, Qt +from qgis.PyQt.QtGui import QStandardItemModel + +from profile_manager.profiles.utils import qgis_profiles_path + + +class ProfileListModel(QStandardItemModel): + """QStandardItemModel to display available QGIS profile list""" + + NAME_COL = 0 + + def __init__(self, parent: QObject = None): + """ + QStandardItemModel for profile list display + + Args: + parent: QObject parent + """ + super().__init__(parent) + self.setHorizontalHeaderLabels([self.tr("Name")]) + + self.profile_manager = QgsUserProfileManager(str(qgis_profiles_path())) + + # Connect to profile changes + self.profile_manager.setNewProfileNotificationEnabled(True) + self.profile_manager.profilesChanged.connect(self._update_available_profiles) + + # Initialization of available profiles + self._update_available_profiles() + + def flags(self, index: QModelIndex) -> Qt.ItemFlags: + """Define flags for an index. + Used to disable edition. + + Args: + index (QModelIndex): data index + + Returns: + Qt.ItemFlags: flags + """ + default_flags = super().flags(index) + return default_flags & ~Qt.ItemIsEditable # Disable editing + + def _update_available_profiles(self) -> None: + """Update model with all available profiles in manager""" + self.removeRows(0, self.rowCount()) + for profile_name in self.profile_manager.allProfiles(): + self.insert_profile(profile_name) + + def insert_profile(self, profile_name: str) -> None: + """Insert profile in model + + Args: + profile_name (str): profile name + """ + # Get user profile + profile: QgsUserProfile = self.profile_manager.profileForName(profile_name) + if profile: + row = self.rowCount() + 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 + ) + + active_profile_folder_name = Path(QgsApplication.qgisSettingsDirPath()).name + profile_folder_name = Path(profile.folder()).name + 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) diff --git a/profile_manager/profile_manager.py b/profile_manager/profile_manager.py index d4f4892..f461a29 100644 --- a/profile_manager/profile_manager.py +++ b/profile_manager/profile_manager.py @@ -27,7 +27,6 @@ from os import path from pathlib import Path from shutil import copytree -from sys import platform # PyQGIS from qgis.core import Qgis, QgsMessageLog, QgsUserProfileManager @@ -49,6 +48,7 @@ from profile_manager.gui.interface_handler import InterfaceHandler from profile_manager.profile_manager_dialog import ProfileManagerDialog from profile_manager.profiles.profile_action_handler import ProfileActionHandler +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 @@ -251,38 +251,7 @@ def run(self): def set_paths(self): """Sets various OS and profile dependent paths""" - home_path = Path.home() - if platform.startswith("win32"): - self.qgis_profiles_path = ( - f"{home_path}/AppData/Roaming/QGIS/QGIS3/profiles".replace("\\", "/") - ) - self.ini_path = ( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesSource.currentText() - + "/QGIS/QGIS3.ini" - ) - self.operating_system = "windows" - elif platform == "darwin": - self.qgis_profiles_path = ( - f"{home_path}/Library/Application Support/QGIS/QGIS3/profiles" - ) - self.ini_path = ( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesSource.currentText() - + "/qgis.org/QGIS3.ini" - ) - self.operating_system = "mac" - else: - self.qgis_profiles_path = f"{home_path}/.local/share/QGIS/QGIS3/profiles" - self.ini_path = ( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesSource.currentText() - + "/QGIS/QGIS3.ini" - ) - self.operating_system = "unix" + self.qgis_profiles_path = str(qgis_profiles_path()) self.backup_path = adjust_to_operating_system( str(Path.home()) + "/QGIS Profile Manager Backup/" @@ -487,36 +456,13 @@ def get_profile_paths(self) -> tuple[str, str]: def get_ini_paths(self): """Gets path to current chosen source and target qgis.ini file""" - if self.operating_system == "mac": - ini_path_source = adjust_to_operating_system( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesSource.currentText() - + "/qgis.org/QGIS3.ini" - ) - ini_path_target = adjust_to_operating_system( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesTarget.currentText() - + "/qgis.org/QGIS3.ini" - ) - else: - ini_path_source = adjust_to_operating_system( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesSource.currentText() - + "/QGIS/QGIS3.ini" - ) - ini_path_target = adjust_to_operating_system( - self.qgis_profiles_path - + "/" - + self.dlg.comboBoxNamesTarget.currentText() - + "/QGIS/QGIS3.ini" - ) - ini_paths = { - "source": ini_path_source, - "target": ini_path_target, + "source": str( + get_profile_qgis_ini_path(self.dlg.comboBoxNamesSource.currentText()) + ), + "target": str( + get_profile_qgis_ini_path(self.dlg.comboBoxNamesTarget.currentText()) + ), } return ini_paths diff --git a/profile_manager/profile_manager_dialog.py b/profile_manager/profile_manager_dialog.py index 12639bd..afc68ae 100644 --- a/profile_manager/profile_manager_dialog.py +++ b/profile_manager/profile_manager_dialog.py @@ -21,9 +21,22 @@ ***************************************************************************/ """ +# standard import os +from pathlib import Path +from typing import Optional +# pyQGIS from qgis.PyQt import QtWidgets, uic +from qgis.PyQt.QtWidgets import QMessageBox + +# plugin +from profile_manager.gui.mdl_profiles import ProfileListModel +from profile_manager.qdt_export.profile_export import ( + QDTProfileInfos, + export_profile_for_qdt, + get_qdt_profile_infos_from_file, +) # This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer FORM_CLASS, _ = uic.loadUiType( @@ -41,3 +54,84 @@ def __init__(self, parent=None): # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html # #widgets-and-dialogs-with-auto-connect self.setupUi(self) + + self.profile_mdl = ProfileListModel(self) + self.qdt_export_profile_cbx.setModel(self.profile_mdl) + self.export_qdt_button.clicked.connect(self.export_qdt_handler) + self.export_qdt_button.setEnabled(False) + self.qdt_file_widget.fileChanged.connect(self._qdt_export_dir_changed) + + self.comboBoxNamesSource.setModel(self.profile_mdl) + self.comboBoxNamesTarget.setModel(self.profile_mdl) + self.list_profiles.setModel(self.profile_mdl) + + def get_list_selection_profile_name(self) -> Optional[str]: + """Get selected profile name from list + + Returns: + Optional[str]: selected profile name, None if no profile selected + """ + index = self.list_profiles.selectionModel().currentIndex() + if index.isValid(): + return self.list_profiles.model().data(index, ProfileListModel.NAME_COL) + return None + + def _qdt_export_dir_changed(self) -> None: + """Update UI when QDT export dir is changed: + - enabled/disable button + - define QDTProfileInformations if profile.json file is available + """ + export_dir = self.qdt_file_widget.filePath() + if export_dir: + self.export_qdt_button.setEnabled(True) + profile_json = Path(export_dir) / "profile.json" + if profile_json.exists(): + self._set_qdt_profile_infos( + get_qdt_profile_infos_from_file(profile_json) + ) + else: + self.export_qdt_button.setEnabled(False) + + def _get_qdt_profile_infos(self) -> QDTProfileInfos: + """Get QDTProfileInfos from UI + + Returns: + QDTProfileInfos: QDT Profile Information + """ + return QDTProfileInfos( + description=self.qdt_description_edit.toPlainText(), + email=self.qdt_email_edit.text(), + version=self.qdt_version_edit.text(), + qgis_min_version=self.qdt_qgis_min_version_edit.text(), + qgis_max_version=self.qdt_qgis_max_version_edit.text(), + ) + + def _set_qdt_profile_infos(self, qdt_profile_infos: QDTProfileInfos) -> None: + """Set QDTProfileInfos in UI + + Args: + qdt_profile_infos (QDTProfileInfos): QDT Profile Information + """ + self.qdt_description_edit.setPlainText(qdt_profile_infos.description) + self.qdt_email_edit.setText(qdt_profile_infos.email) + self.qdt_version_edit.setText(qdt_profile_infos.version) + self.qdt_qgis_min_version_edit.setText(qdt_profile_infos.qgis_min_version) + self.qdt_qgis_max_version_edit.setText(qdt_profile_infos.qgis_max_version) + + def export_qdt_handler(self) -> None: + """Export selected profile as QDT profile""" + profile_path = self.qdt_file_widget.filePath() + if profile_path: + source_profile_name = self.qdt_export_profile_cbx.currentText() + export_profile_for_qdt( + profile_name=source_profile_name, + export_path=Path(profile_path), + qdt_profile_infos=self._get_qdt_profile_infos(), + clear_export_path=self.qdt_clear_export_folder_checkbox.isChecked(), + export_inactive_plugin=self.qdt_inactive_plugin_export_checkbox.isChecked(), + ) + QMessageBox.information( + self, + self.tr("QDT profile export"), + self.tr("QDT profile have been successfully exported."), + ) diff --git a/profile_manager/profile_manager_dialog_base.ui b/profile_manager/profile_manager_dialog_base.ui index 41c9ef2..0c8d7fc 100644 --- a/profile_manager/profile_manager_dialog_base.ui +++ b/profile_manager/profile_manager_dialog_base.ui @@ -6,8 +6,8 @@ 0 0 - 733 - 622 + 701 + 503 @@ -20,7 +20,7 @@ - 0 + 2 @@ -97,7 +97,7 @@ - + @@ -193,7 +193,7 @@ - 0 + 2 @@ -345,6 +345,127 @@ + + + QDT Export + + + + + + + 0 + 0 + + + + Profile + + + + + + + QGIS min. version + + + + + + + Description + + + + + + + + + + Email + + + + + + + + + + + + + Clear export folder + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Export inactive plugins + + + + + + + Version + + + + + + + Export + + + + + + + QGIS max. version + + + + + + + + + + + + + QgsFileWidget::GetDirectory + + + + + + + Export folder + + + + + @@ -356,6 +477,13 @@ + + + QgsFileWidget + QWidget +
qgsfilewidget.h
+
+
diff --git a/profile_manager/profiles/profile_copier.py b/profile_manager/profiles/profile_copier.py index d1429b7..b5e037c 100644 --- a/profile_manager/profiles/profile_copier.py +++ b/profile_manager/profiles/profile_copier.py @@ -15,9 +15,9 @@ def __init__(self, profile_manager_dialog, qgis_path, *args, **kwargs): self.qgis_path = qgis_path def copy_profile(self): - source_profile = self.dlg.list_profiles.currentItem() + 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.text() + "/" + source_profile_path = self.qgis_path + "/" + source_profile + "/" dialog = NameProfileDialog() return_code = dialog.exec() diff --git a/profile_manager/profiles/profile_creator.py b/profile_manager/profiles/profile_creator.py index 5abc03f..5dfa3e6 100644 --- a/profile_manager/profiles/profile_creator.py +++ b/profile_manager/profiles/profile_creator.py @@ -1,4 +1,5 @@ from os import mkdir +from sys import platform from qgis.core import QgsUserProfileManager from qgis.PyQt.QtWidgets import QDialog, QMessageBox @@ -27,7 +28,7 @@ def create_new_profile(self): assert profile_name != "" # should be forced by the GUI self.qgs_profile_manager.createUserProfile(profile_name) try: - if self.profile_manager.operating_system == "mac": + if platform == "darwin": profile_path = ( self.qgis_path + "/" + profile_name + "/qgis.org/" ) diff --git a/profile_manager/profiles/profile_editor.py b/profile_manager/profiles/profile_editor.py index 8ffb2d9..1bbe061 100644 --- a/profile_manager/profiles/profile_editor.py +++ b/profile_manager/profiles/profile_editor.py @@ -21,7 +21,7 @@ def __init__( def edit_profile(self): """Renames profile with user input""" - old_profile_name = self.dlg.list_profiles.currentItem().text() + 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 diff --git a/profile_manager/profiles/profile_remover.py b/profile_manager/profiles/profile_remover.py index a2bb5f8..eec47f1 100644 --- a/profile_manager/profiles/profile_remover.py +++ b/profile_manager/profiles/profile_remover.py @@ -23,12 +23,11 @@ def remove_profile(self): Aborts and shows an error message if no backup could be made. """ - profile_item = self.dlg.list_profiles.currentItem() + profile_name = self.dlg.get_list_selection_profile_name() # bad states that should be prevented by the GUI - assert profile_item is not None - assert profile_item.text() != Path(QgsApplication.qgisSettingsDirPath()).name + assert profile_name is not None + assert profile_name != Path(QgsApplication.qgisSettingsDirPath()).name - profile_name = profile_item.text() profile_path = adjust_to_operating_system(self.qgis_path + "/" + profile_name) clicked_button = QMessageBox.question( diff --git a/profile_manager/profiles/utils.py b/profile_manager/profiles/utils.py new file mode 100644 index 0000000..f7928e5 --- /dev/null +++ b/profile_manager/profiles/utils.py @@ -0,0 +1,227 @@ +from configparser import NoSectionError, RawConfigParser +from dataclasses import dataclass +from pathlib import Path +from sys import platform +from typing import Any, Dict, List, Optional + +import pyplugin_installer +from qgis.core import QgsUserProfileManager +from qgis.utils import iface + + +def qgis_profiles_path() -> Path: + """Get QGIS profiles paths from current QGIS application + + Returns: + Path: QGIS profiles path + """ + return Path(iface.userProfileManager().rootLocation()) + + +def get_profile_qgis_ini_path(profile_name: str) -> Path: + """Get QGIS3.ini file path for a profile + + Args: + profile_name (str): profile name + + Returns: + Path: QGIS3.ini path + """ + # MacOS + if platform.startswith("darwin"): + return qgis_profiles_path() / profile_name / "qgis.org" / "QGIS3.ini" + # Windows / Linux + return qgis_profiles_path() / profile_name / "QGIS" / "QGIS3.ini" + + +def get_profile_plugin_metadata_path(profile_name: str, plugin_slug_name: str) -> Path: + """Get path to metadata.txt for a plugin inside a profile + + Args: + profile_name (str): profile name + plugin_slug_name (str): plugin slug name + + Returns: + Path: metadata.txt path + """ + return ( + qgis_profiles_path() + / profile_name + / "python" + / "plugins" + / plugin_slug_name + / "metadata.txt" + ) + + +def get_installed_plugin_list( + profile_name: str, only_activated: bool = True +) -> List[str]: + """Get installed plugin for a profile + + Args: + profile_name (str): profile name + only_activated (bool, optional): True to get only activated plugin, False to get all installed plugins. Defaults to True. + + Returns: + List[str]: plugin slug name list + """ + ini_parser = RawConfigParser() + ini_parser.optionxform = str # str = case-sensitive option names + ini_parser.read(get_profile_qgis_ini_path(profile_name)) + try: + plugins_in_profile = dict(ini_parser.items("PythonPlugins")) + except NoSectionError: + plugins_in_profile = {} + + if only_activated: + return [key for key, value in plugins_in_profile.items() if value == "true"] + else: + return plugins_in_profile.keys() + + +def get_installed_plugin_metadata( + profile_name: str, plugin_slug_name: str +) -> Dict[str, Any]: + """Get metadata information from metadata.txt file in profile installed plugin + + Args: + profile_name (str): profile name + plugin_slug_name (str): plugin slug name + + Returns: + Dict[str, Any]: metadata as dict. Empty dict if metadata unavailable + """ + ini_parser = RawConfigParser() + ini_parser.optionxform = str # str = case-sensitive option names + ini_parser.read(get_profile_plugin_metadata_path(profile_name, plugin_slug_name)) + try: + metadata = dict(ini_parser.items("general")) + except NoSectionError: + metadata = {} + return metadata + + +def get_plugin_info_from_qgis_manager( + plugin_slug_name: str, reload_manager: bool = False +) -> Optional[Dict[str, str]]: + """Get plugin informations from QGIS plugin manager + + Args: + plugin_slug_name (str): _description_ + reload_manager (bool, optional): reload manager for new plugins. Defaults to False. + + Returns: + Optional[Dict[str, str]]: metadata from plugin manager, None if plugin not found + """ + if reload_manager: + pyplugin_installer.instance().reloadAndExportData() + return iface.pluginManagerInterface().pluginMetadata(plugin_slug_name) + + +def get_profile_name_list() -> List[str]: + """Get profile name list from current installed QGIS + + Returns: + List[str]: profile name list + """ + return QgsUserProfileManager(qgis_profiles_path()).allProfiles() + + +@dataclass +class PluginInformation: + name: str + folder_name: str + official_repository: bool + plugin_id: str + version: str + + +def define_plugin_version_from_metadata( + manager_metadata: Dict[str, Any], plugin_metadata: Dict[str, Any] +) -> str: + """Define plugin version from available metadata + + Args: + manager_metadata (Dict[str, Any]): QGIS plugin manager metadata + plugin_metadata (Dict[str, Any]): installed plugin metadata + + Returns: + str: plugin version + """ + # Use version from plugin metadata + if "version" in plugin_metadata: + return plugin_metadata["version"] + + # Fallback to stable version + if manager_metadata["version_available_stable"]: + return manager_metadata["version_available_stable"] + # Fallback to experimental version + if manager_metadata["version_available_experimental"]: + return manager_metadata["version_available_experimental"] + # Fallback to available version + if manager_metadata["version_available"]: + return manager_metadata["version_available"] + # No version defined + return "" + + +def get_profile_plugin_information( + profile_name: str, plugin_slug_name: str +) -> Optional[PluginInformation]: + """Get plugin information from profile. Only official plugin are supported. + + Args: + profile_name (str): profile name + plugin_slug_name (str): plugin slug name + + Returns: + Optional[PluginInformation]: plugin information, None if plugin is not official + """ + manager_metadata = get_plugin_info_from_qgis_manager( + plugin_slug_name=plugin_slug_name + ) + plugin_metadata = get_installed_plugin_metadata( + profile_name=profile_name, plugin_slug_name=plugin_slug_name + ) + + # For now we don't support unofficial plugins + if manager_metadata is None: + return None + + return PluginInformation( + name=manager_metadata["name"], + folder_name=plugin_slug_name, + official_repository=True, # For now we only support official repository + plugin_id=manager_metadata["plugin_id"], + version=define_plugin_version_from_metadata( + manager_metadata=manager_metadata, + plugin_metadata=plugin_metadata, + ), + ) + + +def get_profile_plugin_list_information( + profile_name: str, only_activated: bool = True +) -> List[PluginInformation]: + """Get profile plugin information + + Args: + profile_name (str): profile name + only_activated (bool, optional): True to get only activated plugin, False to get all installed plugins. Defaults to True. + + Returns: + List[PluginInformation]: list of PluginInformation + """ + plugin_list: List[str] = get_installed_plugin_list( + profile_name=profile_name, only_activated=only_activated + ) + # Get information about installed plugin + profile_plugin_list: List[PluginInformation] = [] + + for plugin_name in plugin_list: + plugin_info = get_profile_plugin_information(profile_name, plugin_name) + if plugin_info and plugin_info.plugin_id != "": + profile_plugin_list.append(plugin_info) + + return profile_plugin_list diff --git a/profile_manager/qdt_export/profile_export.py b/profile_manager/qdt_export/profile_export.py new file mode 100644 index 0000000..fc3f84c --- /dev/null +++ b/profile_manager/qdt_export/profile_export.py @@ -0,0 +1,130 @@ +import dataclasses +import json +from dataclasses import dataclass +from pathlib import Path +from shutil import copytree, rmtree +from typing import Any, Dict + +import pyplugin_installer + +from profile_manager.profiles.utils import ( + get_profile_plugin_list_information, + qgis_profiles_path, +) + +QDT_PROFILE_SCHEMA = "https://raw.githubusercontent.com/Guts/qgis-deployment-cli/main/docs/schemas/profile/qgis_profile.json" + + +@dataclass +class QDTProfileInfos: + """Store informations for QDT profile creation""" + + description: str = "" + email: str = "" + version: str = "" + qgis_min_version: str = "" + qgis_max_version: str = "" + + +def get_qdt_profile_infos_from_file(profile_file: Path) -> QDTProfileInfos: + """Get QDT Profile informations from a profile.json file + File must exists + + Args: + profile_file (Path): profile.json path + + Returns: + QDTProfileInfos: QDT Profile informations + """ + with open(profile_file, "r") as f: + qdt_profile_data = json.load(f) + return QDTProfileInfos( + description=qdt_profile_data.get("description", ""), + email=qdt_profile_data.get("email", ""), + version=qdt_profile_data.get("version", ""), + qgis_min_version=qdt_profile_data.get("qgisMinimumVersion", ""), + qgis_max_version=qdt_profile_data.get("qgisMaximumVersion", ""), + ) + + +def qdt_profile_dict( + profile_name: str, + qdt_profile_infos: QDTProfileInfos, + export_inactive_plugin: bool = False, +) -> Dict[str, Any]: + """Create QDT profile dict from QGIS profile + + Get informations from installed plugin and QDT profile informations + + Args: + profile_name (str): profile name + qdt_profile_infos (QDTProfileInfos): information for QDT profile creation + export_inactive_plugin (bool, optional): True for inactive profile plugin export. Defaults to False. + + Returns: + Dict[str, Any]: QDT profile dict + """ + # Get profile installed plugin + only_activated = not export_inactive_plugin + profile_plugin_list = get_profile_plugin_list_information( + profile_name=profile_name, only_activated=only_activated + ) + + return { + "$schema": QDT_PROFILE_SCHEMA, + "name": profile_name, + "folder_name": profile_name, # TODO check for profile with space + "description": qdt_profile_infos.description, + "email": qdt_profile_infos.email, + "icon": "TDB", # TODO add icon + "qgisMinimumVersion": qdt_profile_infos.qgis_min_version, + "qgisMaximumVersion": qdt_profile_infos.qgis_max_version, + "version": qdt_profile_infos.version, + "plugins": [dataclasses.asdict(plugin) for plugin in profile_plugin_list], + } + + +def export_profile_for_qdt( + profile_name: str, + export_path: Path, + qdt_profile_infos: QDTProfileInfos, + clear_export_path: bool = False, + export_inactive_plugin: bool = False, +) -> None: + """Export QGIS profile for QDT + + Args: + profile_name (str): name of profile to export + export_path (Path): export path for QDT profile + qdt_profile_infos (QDTProfileInfos): information for QDT profile creation + clear_export_path (bool, optional): True for export path clear before export. Defaults to False. + export_inactive_plugin (bool, optional): True for inactive profile plugin export. Defaults to False. + """ + pyplugin_installer.instance().reloadAndExportData() + + if clear_export_path: + # Delete current export content + rmtree(export_path, ignore_errors=True) + + # Copy profile content to export path + copytree( + src=Path(qgis_profiles_path()) / profile_name, + dst=export_path, + dirs_exist_ok=True, + ) + + # Delete cache content + rmtree(export_path / "cache", ignore_errors=True) + rmtree(export_path / "oauth2-cache", ignore_errors=True) + + # Delete python/plugins content + rmtree(export_path / "python" / "plugins", ignore_errors=True) + + profile_dict = qdt_profile_dict( + profile_name=profile_name, + qdt_profile_infos=qdt_profile_infos, + export_inactive_plugin=export_inactive_plugin, + ) + + with open(export_path / "profile.json", "w", encoding="UTF-8") as f: + json.dump(profile_dict, f, indent=4) diff --git a/profile_manager/resources/i18n/plugin_translation.pro b/profile_manager/resources/i18n/plugin_translation.pro index 944de25..a72c906 100644 --- a/profile_manager/resources/i18n/plugin_translation.pro +++ b/profile_manager/resources/i18n/plugin_translation.pro @@ -7,10 +7,13 @@ SOURCES = \ ../../profiles/profile_remover.py \ ../../profiles/profile_creator.py \ ../../profiles/profile_action_handler.py \ + ../../profiles/utils.py \ ../../profile_manager_dialog.py \ ../../gui/interface_handler.py \ ../../gui/name_profile_dialog.py \ + ../../gui/mdl_profiles.py \ ../../profile_manager.py \ + ../../qdt_export/profile_export.py \ ../../datasources/functions/function_handler.py \ ../../datasources/dataservices/datasource_distributor.py \ ../../datasources/dataservices/datasource_provider.py \