Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
f7945dc
Cosmetic cleanups during code reading
jk-wg May 26, 2023
39a4a49
Fix typos
jk-wg May 30, 2023
38a4bea
PyQt imports via qgis.PyQt, exec_ -> exec
jk-wg May 30, 2023
e7324f1
Use with-context for writing file
jk-wg May 30, 2023
56f8f0e
Adjust super() calls to modern Python
jk-wg May 30, 2023
8b51c7d
Use dialogs and log entries instead of print
jk-wg May 30, 2023
6ff15ea
Remove redundant rmdir that was always raising an error
jk-wg May 30, 2023
97b5866
Fix translation callers where not wanted
jk-wg May 30, 2023
1cd50fa
Fix typo
jk-wg May 30, 2023
d2f7d57
Fix wrong method name, refactor method to create bookmark file
jk-wg May 30, 2023
978aec3
Use int(time.time()) for timestamp instead of time+calendar
jk-wg May 31, 2023
d4499fc
Remove unnecessary iface import
jk-wg May 31, 2023
c911c82
Fix docstrings
jk-wg May 31, 2023
45184c5
Don't alter instance variable when GPKG is handled
jk-wg May 31, 2023
df8f627
Remove unused parameter
jk-wg May 31, 2023
a172b65
Remove version check, metadata.txt already defines min version
jk-wg Jun 12, 2023
1fa2ece
Don't remove read-only from dirs, show error dialogs instead
jk-wg Jun 12, 2023
fd613b0
Change backup path to "QGIS Profile Manager Backup"
jk-wg Jun 12, 2023
9c428a5
Adjust plugin list population code to actual implementation
jk-wg Jun 12, 2023
7fabad0
Simplify and refactor dialogs
jk-wg Jun 13, 2023
230a455
Prevent the user from doing unwanted actions
jk-wg Jun 13, 2023
add0d6d
Remove unnecessary instance variables
jk-wg Jun 13, 2023
e3faea4
Highlight current profile's name here and there
jk-wg Jun 13, 2023
08689fb
Rename, document and refactor some functions
jk-wg Jun 13, 2023
3e2411a
Preselect current profile as source profile
jk-wg Jun 13, 2023
69489d1
Remove deprecated method
jk-wg Jun 13, 2023
709af1a
Improved documentation, names
jk-wg Jun 13, 2023
acd4762
Make dialog modal to avoid mismatches between QGIS and plugin
jk-wg Jun 13, 2023
11d1543
Fix slow start and redisplay
jk-wg Jun 13, 2023
4535c90
Don't show wait cursor on top of dialog messages
jk-wg Jun 14, 2023
0d2fb23
Harmonize and tone down messages, include variable values
jk-wg Jun 14, 2023
bdc71be
Adjust texts in main dialog
jk-wg Jun 14, 2023
7b05955
Set layouts and spacers for main dialog
jk-wg Jun 14, 2023
8c0e9c2
Adjust more texts in main dialog
jk-wg Jun 14, 2023
64a8131
Disallow slashes in profile name (ported from QGIS)
jk-wg Jun 14, 2023
19a174e
Adjust import order
jk-wg Jun 14, 2023
2a15124
Minimize try/except blocks, collect items in list, log msgs
jk-wg Jun 14, 2023
27eadb4
Improve try/except blocks
jk-wg Jun 14, 2023
cce5d93
Fix: Missed import adjustments from earlier commit
jk-wg Jun 14, 2023
7005194
Fix useless message from data source scanning
jk-wg Jun 14, 2023
137677f
Log name of scanned profile
jk-wg Jun 14, 2023
18fea3d
Fix: Correct initial tab of main dialog
jk-wg Jun 14, 2023
741dac2
Eradicate some more "except Exception"s
jk-wg Jun 15, 2023
0863cb1
Rename functions to reflect what they actually do
jk-wg Jun 15, 2023
a4a947a
Add documentation for handlers
jk-wg Jun 15, 2023
6e65fab
Fix typos
jk-wg Jun 15, 2023
de9060e
Handle errors in handlers with user feedback
jk-wg Jun 15, 2023
1f3d5bf
Remove needlessly detailed exceptions for file operations
jk-wg Jun 16, 2023
53b92eb
Wording: Prefer "remove" to "delete", like in QGIS
jk-wg Jun 16, 2023
50bb5b4
Improve messages (quoting, trailing punctuation)
jk-wg Jun 16, 2023
e477a39
Fix default tab index in dialog
jk-wg Jun 16, 2023
0a8913e
Add some tooltips for better UX
jk-wg Jun 16, 2023
ff8d021
Update translations (German is complete, Italian out of date)
jk-wg Jun 16, 2023
25fc415
Update metadata for upcoming 0.5, remove old changelog
jk-wg Jun 16, 2023
93d178f
Remove unused Plugin Builder clutter
jk-wg Jun 16, 2023
2a5d9d2
Remove resources.py
jk-wg Jun 16, 2023
392bb3f
Drop unnecessary u""
jk-wg Jun 16, 2023
9c949ad
Prevent crash when locale is unset
jk-wg Jun 16, 2023
7de675e
Fix name dialog's widgets to resize on resize
jk-wg Jun 16, 2023
f76a688
Refactor unnecessary instance variables into local variables
jk-wg Jun 28, 2023
1e4a64e
Rename provider names like in QGIS
jk-wg Jun 28, 2023
e6e30b4
Simplify determination of web or database source type
jk-wg Jun 28, 2023
4306866
Typo fix, comments
jk-wg Jun 28, 2023
310f1d9
Fix mistake from refactoring
jk-wg Jun 28, 2023
4c7819c
Revert renamings from 1e4a64e to fix break, add warnings and FIXMEs
jk-wg Jun 29, 2023
a0c1e3f
Fix missing import
jk-wg Jun 29, 2023
25013a4
Add support for vector tile connections
jk-wg Jun 29, 2023
b1007f3
Some informative logging during data source connection discovery
jk-wg Jun 29, 2023
f0c7e5f
Remove dubious replace(" - ", "") from profile names
jk-wg Jun 29, 2023
b47745e
Only backup altered profile, not all profiles, to reduce storage size
jk-wg Jun 29, 2023
1b1b9ba
Improve and update readme with known limitations, screenshots, etc
jk-wg Jun 29, 2023
0e38fd8
Update translations, add .pro file
jk-wg Jun 29, 2023
8ba22ff
Fix pb_tool.cfg to actually work for this plugin
jk-wg Jun 29, 2023
b065859
Fix missing import, add another FIXME comment, improve docstring
jk-wg Jun 29, 2023
da2ce40
Update metadata.txt
jk-wg Jun 29, 2023
5c52c37
Select current profile by default
jk-wg Jun 29, 2023
1f75f7b
Merge branch 'main' into cleanup_2023
jk-wg Jun 29, 2023
35ed519
Fix copy button activation when current profile is first selected
jk-wg Jun 30, 2023
afbe6f3
Add .idea and zip_build to .gitignore
jk-wg Jun 30, 2023
121b021
Remove unused icons directory from pb_tool.conf
jk-wg Jun 30, 2023
d9fd747
Add translations directory to pb_tool.cfg
jk-wg Jun 30, 2023
5c83ab6
Add grassprovider to list of core plugins to be hidden
jk-wg Jul 3, 2023
f07240b
Add more (potential) plugins to list of core plugins to be hidden
jk-wg Jul 3, 2023
4ba737c
Sort and document list of core plugins
jk-wg Jul 3, 2023
f5425e1
Display all plugins for import, not just activated ones
jk-wg Jul 3, 2023
05761cc
Refactor and simplify code around plugins
jk-wg Jul 3, 2023
d15ef5f
Remove broken function to migration plugin *options*
jk-wg Jul 3, 2023
377d51e
Document "parser.optionxform = str" -> case sensitive option names
jk-wg Jul 3, 2023
1968c7f
Don't add spaces around delimiters in INI files (match QGIS)
jk-wg Jul 3, 2023
bee5410
Disable selection of target items, and core plugins in general
jk-wg Jul 3, 2023
4beddc5
Extract adjust_to_operating_system path method into utils functions
jk-wg Jul 3, 2023
0ff9b64
Move dialog related method nearer to its dialog
jk-wg Jul 19, 2023
580ef13
Fix broken platform detection
jk-wg Jul 19, 2023
f86b819
Remove unused method
jk-wg Jul 19, 2023
c2e59d7
Remove unused instance variables
jk-wg Jul 19, 2023
eaf9d1f
Fix typo, add important TODO/FIXME
jk-wg Jul 19, 2023
752530a
Remove unused instance variable
jk-wg Jul 19, 2023
effd856
Prefer single-use parsers in methods, not as instance variables
jk-wg Jul 19, 2023
0bb8862
Refactor handler classes into simple functions
jk-wg Jul 20, 2023
3b43feb
Remove unnecessary setup of DS distributor when importing plugins
jk-wg Jul 20, 2023
763c736
Refactor data source distributor into simple functions
jk-wg Jul 20, 2023
233c0ce
Refactor plugin import/remove into simple functions, add tr() function
jk-wg Jul 20, 2023
e569e5c
Refactor data source provider into simple functions
jk-wg Jul 20, 2023
af16ccf
Refactor data source provider into simple functions
jk-wg Jul 21, 2023
b70a2ca
Move provider/datasource search rules into single object, refactor
jk-wg Jul 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.idea
.pyc
__pycache__
.pyc
zip_build
37 changes: 0 additions & 37 deletions CHANGELOG

This file was deleted.

91 changes: 56 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,63 @@
### Profile Manager for QGIS3 ###
### Profile Manager for QGIS ###
A QGIS plugin for managing your profiles and data source connections.

### Plugin for managing your profiles and datasources ###
https://plugins.qgis.org/plugins/profile-manager/

### Installation ###
<a href="https://github.com/WhereGroup/profile-manager/assets/7661092/a64ea854-bff3-48a0-b1ff-83f3d4eaa5b7"><img src="https://github.com/WhereGroup/profile-manager/assets/7661092/a64ea854-bff3-48a0-b1ff-83f3d4eaa5b7" width="200"></a>
<a href="https://github.com/WhereGroup/profile-manager/assets/7661092/711faa86-c36c-40bc-92ab-7b73016996ae"><img src="https://github.com/WhereGroup/profile-manager/assets/7661092/711faa86-c36c-40bc-92ab-7b73016996ae" width="200"></a>
<a href="https://github.com/WhereGroup/profile-manager/assets/7661092/0c646930-88d8-45fe-81c8-4a5bf4501152"><img src="https://github.com/WhereGroup/profile-manager/assets/7661092/0c646930-88d8-45fe-81c8-4a5bf4501152" width="200"></a>
<a href="https://github.com/WhereGroup/profile-manager/assets/7661092/079665d6-e0ff-45fb-a65c-3b49cd9229de"><img src="https://github.com/WhereGroup/profile-manager/assets/7661092/079665d6-e0ff-45fb-a65c-3b49cd9229de" width="200"></a>

To install the plugin just copy the folder in your QGIS-Profile folder under ./python/plugins/
### Installation ###
To install the plugin manually just copy the folder into your QGIS profile directory under ./python/plugins/

- Windows Directory:
- C:\Users\{USER}\AppData\Roaming\QGIS\QGIS3\profiles\\{PROFILE}\python\plugins\
- Linux Directory:
- /home/{USER}/.local/share/QGIS/QGIS3/profiles/{PROFILE}/python/plugins
- Windows directory:
- `C:\Users\{USER}\AppData\Roaming\QGIS\QGIS3\profiles\{PROFILE}\python\plugins\`
- Linux directory:
- `~/.local/share/QGIS/QGIS3/profiles/{PROFILE}/python/plugins`
- MacOS directory:
- `~/Library/Application Support/QGIS/QGIS3/profiles`

### Features ###
- Create a new profile
- Creates and initiates a new profile
- Removing profile
- Removes a selected profile
- Copy profile
- Creates a copy of a selected profile with a new name
- Rename profile
- Renames the profile with a name provided by the user
- Importing data source connections from one profile to another
- Removing data source connections from a profile
- Removes the data source connection from the chosen SOURCE profile
- Importing (spatial) bookmarks
- Importing (data source) favourites
- Importing plugins
- Importing expression functions
- Importing models & scripts
- Importing some symbology types & label settings
- Importing QGIS UI settings (e.g. hidden toolbar items)

On all removal operations the user is being asked if they are certain
that he wants to delete given source/profile.
Additionally before every deletion a backup of the complete profiles
folder is created under the following directory:
- Windows directory:
- `C:\Users\{USER}\QGIS Profile Manager Backup\`
- Linux and MacOS directory:
- `~/QGIS Profile Manager Backup/`

### Known (current) limitations ###
- Not all data source connections might be recognized and imported/removed
- Not all data source connection types are supported
- Python expression functions are not supported
- Not all style things are supported, e.g. not 3D symbols, color ramps,
tags, etc.
- Errors might not always be communicated clearly so please TEST your
migrated configurations before discarding originals!

- General Features:
- Create a new profile
- Creates and initiates a new QGIS profile
- Reomving profile
- Removes the currently marked profile
- Copy profile
- Copies profile into a new one with a name provided by the user
- Rename profile
- Renames the profile with a name provided by the user
- Importing Datasources from one profile to another
(Datasources are chosen by checking them in a TreeView where each source is displayed)
- Removing Datasources from a profile
- Removes the datasources from the chosen SOURCE profile
- Importing bookmarks
- Importing favourites
- Importing plugins
- Importing functions
- Importing models & scripts
- Importing style & label settings
- Importing QGIS UI settings (f.e. hidden toolbar items)

On all removal operations the user is being asked if he is certain that he wants to delete given source/profile.
Additionally before every deletion a backup of the profiles folder is created under the follwing directory:
- Windows Directory:
- C:\Users\\{USER}\QGISBackup\
- Linux Directory:
- /home/{USER}/QGISBackup/
### Funding development ###
If you consider this plugin useful and would like to see it improved, e.g.
with support for more profile settings, becoming more stable, being more
thoroughly documented, leave the "experimental" plugin status or whatever
you desire, you can fund development. Contact us at info@wheregroup.com
1 change: 0 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
/***************************************************************************
ProfileManager
Expand Down
7 changes: 0 additions & 7 deletions compile.bat

This file was deleted.

129 changes: 59 additions & 70 deletions datasources/Bookmarks/bookmark_handler.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
from lxml import etree as et
from pathlib import Path
import sys


class BookmarkHandler:

def __init__(self, qgis_path, profile_manager):
self.profile_manager = profile_manager
self.qgis_path = qgis_path
self.bookmark_list = []
self.source_bookmark_file = ""
self.target_bookmark_file = ""
self.parser = et.XMLParser(remove_blank_text=True)

def parse_source_bookmarks(self):
"""Parses bookmarks from source xml"""
# get the element tree of the source file
try:
source_tree = et.parse(self.source_bookmark_file, self.parser)
self.insert_bookmarks_to_target_profile(source_tree)
except:
print("Oops!", sys.exc_info()[0], "occurred.")

def insert_bookmarks_to_target_profile(self, source_tree):
"""Inserts bookmarks into target xml file"""

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.:
<Bookmarks>
<Bookmark id="..." group="" extent="POLYGON((...))" name="Test Bookmark">
<spatialrefsys nativeFormat="Wkt">
...
</spatialrefsys>
</Bookmark>
...
</Bookmarks>

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
self.check_if_file_exists()
create_bookmark_file_if_not_exist(target_bookmark_file)
# get the element tree of the target file
# fill if empty
try:
target_tree = et.parse(self.target_bookmark_file, self.parser)
except et.XMLSyntaxError:
with open(self.target_bookmark_file, "w") as xmlTarget:
xmlTarget.write("<Bookmarks></Bookmarks>")

target_tree = et.parse(self.target_bookmark_file, self.parser)
target_tree = et.parse(target_bookmark_file, et.XMLParser(remove_blank_text=True))

# find all bookmark elements
source_root_tag = source_tree.findall('Bookmark')
Expand All @@ -43,48 +41,39 @@ def insert_bookmarks_to_target_profile(self, source_tree):
target_tree_root = target_tree.getroot()

# Remove duplicate entries to prevent piling data
target_tree_root = self.remove_duplicates(source_root_tag, target_tree, target_tree_root)
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(self.target_bookmark_file, pretty_print=True, encoding='utf-8', xml_declaration=True)

@staticmethod
def remove_duplicates(source_toot_tag, target_tree, target_tree_root):
"""Removes duplicate entries"""
target_root_tag = target_tree.findall('Bookmark')
for s_element in source_toot_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 check_if_file_exists(self):
"""Checks if file exists"""
target_file = Path(self.target_bookmark_file)
if target_file.is_file():
pass
else:
self.create_new_file()

def create_new_file(self):
"""Creates a new file"""
new_file = open(self.target_bookmark_file, "w")
new_file.write("<Bookmarks></Bookmarks>")
new_file.close()

def set_path_files(self, source_bookmark_file, target_bookmark_file):
"""Sets file path's"""
self.source_bookmark_file = source_bookmark_file
self.target_bookmark_file = target_bookmark_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("<Bookmarks></Bookmarks>")

70 changes: 42 additions & 28 deletions datasources/Customizations/customization_handler.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,54 @@
from shutil import copy2
from os import path
from configparser import RawConfigParser
from os import path
from shutil import copy2

from ...utils import adjust_to_operating_system


def import_customizations(source_profile_path: str, target_profile_path: str):
"""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

TODO fix discrepancy between [UI] and [Customization]! Which one(s) exist and what do we want to transfer?

class CustomizationHandler:
E.g.
[Customization]
Browser=true
Browser\AFS=false
...

def __init__(self, profile_manager):
self.profile_manager = profile_manager
self.path_source_customini = ""
self.path_target_customini = ""
self.parser = RawConfigParser()
self.parser.optionxform = str
Args:
TODO
"""
# 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):
copy2(source_customini_path, target_customini_path)

def import_customizations(self):
if path.exists(self.path_source_customini):
copy2(self.path_source_customini, self.path_target_customini)
# 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")

ini_paths = self.profile_manager.get_ini_paths()
source_ini_parser = RawConfigParser()
source_ini_parser.optionxform = str # str = case-sensitive option names
source_ini_parser.read(source_qgis3ini_path)

self.parser.read(ini_paths["source"])
# TODO this is broken, right? It looks for [UI] but even in QGIS 3.10 (didnt check older) the (single) section is named [Customization]
if source_ini_parser.has_section('UI'):
ui_data = dict(source_ini_parser.items('UI'))

if self.parser.has_section('UI'):
ui_data = dict(self.parser.items('UI'))
self.parser.clear()
self.parser.read(ini_paths["target"])
target_ini_parser = RawConfigParser()
target_ini_parser.optionxform = str # str = case-sensitive option names
target_ini_parser.read(target_qgis3ini_path)

for setting in ui_data:
if not self.parser.has_section("UI"):
self.parser["UI"] = {}

self.parser.set("UI", setting, ui_data[setting])
for setting in ui_data:
if not target_ini_parser.has_section("UI"):
target_ini_parser["UI"] = {}

with open(ini_paths["target"], 'w') as qgisconf:
self.parser.write(qgisconf)
target_ini_parser.set("UI", setting, ui_data[setting])

def set_path_files(self, source, target):
self.path_source_customini = self.profile_manager.adjust_to_operating_system(source + "QGIS/QGISCUSTOMIZATION3.ini")
self.path_target_customini = self.profile_manager.adjust_to_operating_system(target + "QGIS/QGISCUSTOMIZATION3.ini")
with open(target_qgis3ini_path, 'w') as qgisconf:
target_ini_parser.write(qgisconf, space_around_delimiters=False)

Loading