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
+
+
+
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 \