Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 47 additions & 0 deletions docs/usage/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,50 @@ Permet de choisir dans quel navigateur ouvrir les contenus :
## Notification

Activer ou désactiver le message de notification lorsqu'un nouveau contenu a été publié depuis la dernière lecture.

## Variables d’environnement

La plupart des paramètres peuvent être définis via des variables d’environnement. Cela permet de confier la configuration du plugin directement au service informatique. Cette approche offre plusieurs périmètres de portée :

- pour tous les profils QGIS d’un ordinateur (nécessite des droits administrateur)
- pour tous les profils QGIS d’une session utilisateur
- dans un profil QGIS spécifique : voir `Préférences` > `Système` et le panneau `Variables d’environnement` (exemple ici
)
- uniquement pour une session QGIS donnée en les définissant au lancement

Ceci permet également de s'intégrer à des logiques de déploiement de QGIS et des configurations liées au niveau du Système d'Information, comme avec [QGIS Deployment Toolbelt (QDT)](./with_qdt.md) par exemple.

Par exemple, sur un système Linux classique, si vous souhaitez activer les notifications quand il y a un nouveau contenu de publié, qu'elles soient affichées pendant 30 secondes et ouvrir les contenus dans votre navigateur web par défaut, vous pouvez ajouter les lignes suivantes à votre fichier `.bashrc`, `.zshrc` ou `.profile`:

```sh
export QGIS_PLG_QTRIBU_NOTIFY_PUSH_INFO="true"
export QGIS_PLG_QTRIBU_NOTIFY_PUSH_DURATION="30"
export QGIS_PLG_QTRIBU_BROWSER="2"
```

Sous Windows, c’est encore plus simple : une interface graphique permet de définir les variables d’environnement aussi bien au niveau du système (administrateur) que de l’utilisateur. Vous pouvez y accéder via le menu Démarrer.

Le tableau suivant liste les paramètres disponibles, ainsi que la variable d’environnement associée et sa valeur par défaut :

| Paramètre | Variable d'environnement | Valeur par défaut |
| :-------- | :----------------------: | :---------------: |
| Activer le mode debug | `QGIS_PLG_QTRIBU_DEBUG_MODE` | False |
| Dossier local de l'application | `QGIS_PLG_QTRIBU_LOCAL_APP_FOLDER` | PosixPath('/home/jmo/.geotribu/cache') |
| URL du flux JSON | `QGIS_PLG_QTRIBU_JSON_FEED_SOURCE` | '<https://geotribu.fr/feed_json_created.json>' |
| URL du flux RSS | `QGIS_PLG_QTRIBU_RSS_SOURCE` | '<https://geotribu.fr/feed_rss_created.xml>' |
| Fréquence de consultation du RSS | `QGIS_PLG_QTRIBU_RSS_POLL_FREQUENCY_HOURS` | 24 |
| Navigateur web à utiliser (1 : intégré à QGIS ; 2 : navigateur par défaut du système) | `QGIS_PLG_QTRIBU_BROWSER` | 1 |
| Activer les notifications | `QGIS_PLG_QTRIBU_NOTIFY_PUSH_INFO` | True |
| Durée de la notification | `QGIS_PLG_QTRIBU_NOTIFY_PUSH_DURATION` | 10 |
| Afficher le splash screen de Geotribu | `QGIS_PLG_QTRIBU_SPLASH_SCREEN_ENABLED` | False |
| Acceptation de la licence globale | `QGIS_PLG_QTRIBU_LICENSE_GLOBAL_ACCEPT` | False |
| Licence préférée pour mes articles | `QGIS_PLG_QTRIBU_LICENSE_ARTICLE_PREFERRED` | '' |
| Acceptation de la charte éditoriale | `QGIS_PLG_QTRIBU_EDITORIAL_POLICY_ACCEPT` | False |
| Intégration au fil d'actualité de QGIS | `QGIS_PLG_QTRIBU_INTEGRATION_QGIS_NEWS_FEED` | True |
| Prénom | `QGIS_PLG_QTRIBU_AUTHOR_FIRSTNAME` | '' |
| Nom de famille | `QGIS_PLG_QTRIBU_AUTHOR_LASTNAME` | '' |
| Email | `QGIS_PLG_QTRIBU_AUTHOR_EMAIL` | '' |
| Nom d'utilisateur GitHub | `QGIS_PLG_QTRIBU_AUTHOR_GITHUB` | '' |
| Identifiant LinkedIn | `QGIS_PLG_QTRIBU_AUTHOR_LINKEDIN` | '' |
| Nom d'utilisateur Bluesky | `QGIS_PLG_QTRIBU_AUTHOR_BLUESKY` | '' |
| Nom d'utilisateur Mastodon | `QGIS_PLG_QTRIBU_AUTHOR_MASTODON` | '' |
2 changes: 1 addition & 1 deletion qtribu/gui/dlg_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def apply(self):
"""Called to permanently apply the settings shown in the options page (e.g. \
save them to QgsSettings objects). This is usually called when the options \
dialog is accepted."""
settings = self.plg_settings.get_plg_settings()
settings: PlgSettingsStructure = self.plg_settings.get_plg_settings()

# features
settings.browser = self.opt_browser_group.checkedId()
Expand Down
60 changes: 60 additions & 0 deletions qtribu/toolbelt/env_var_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import os
from typing import Type, TypeVar

T = TypeVar("T")


class EnvVarParser:
"""Utility class to retrieve and convert environment variables."""

@staticmethod
def get_env_var(name: str, default: T) -> T:
"""Retrieves an environment variable and converts it based on the default value type.

Args:
name (str): The environment variable name.
default (T): The default value, used to infer the expected type.

Returns:
T: The converted value, matching the type of `default`.
"""
value: str | None = os.getenv(name)
if value is None:
return (
default # Return the default value if the environment variable is not
)

# Otherwise, treat it as a single value
return EnvVarParser._convert_single(value, type(default), default)

@staticmethod
def _convert_single(value: str, expected_type: Type[T], default: T) -> T:
"""Converts a string into a single value of the expected type."""
try:
if expected_type is int:
return int(value)
elif expected_type is float:
return float(value)
elif expected_type is bool:
return EnvVarParser._convert_bool(value, default)
elif expected_type is str:
return value # String value
except ValueError:
return default # Return default value in case of conversion failure

raise TypeError(
f"Unsupported type: {expected_type}. Value definition from environment variable is not possible."
)

@staticmethod
def _convert_bool(value: str, default: bool) -> bool:
"""Converts a string into a boolean, handling explicit True/False values."""
true_values: set[str] = {"1", "true", "yes", "on"}
false_values: set[str] = {"0", "false", "no", "off"}
value_lower: str = value.lower()

if value_lower in true_values:
return True
elif value_lower in false_values:
return False
return default # Return default value if conversion fails
46 changes: 40 additions & 6 deletions qtribu/toolbelt/preferences.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
#! python3 # noqa: E265

"""
Plugin settings.
"""
"""Plugin settings."""

# standard
from dataclasses import asdict, dataclass, fields
from pathlib import Path
from typing import Any

# PyQGIS
from qgis.core import Qgis, QgsSettings
Expand All @@ -15,12 +14,38 @@
import qtribu.toolbelt.log_handler as log_hdlr
from qtribu.__about__ import __title__, __version__
from qtribu.toolbelt.application_folder import get_app_dir
from qtribu.toolbelt.env_var_parser import EnvVarParser

# -- Globals --
PREFIX_ENV_VARIABLE = "QGIS_PLG_QTRIBU_"

# ############################################################################
# ########## Classes ###############
# ##################################


@dataclass
class PlgEnvVariableSettings:
"""Plugin settings from environnement variable"""

def env_variable_used(self, attribute: str, default_from_name: bool = True) -> str:
"""Get environnement variable used for environnement variable settings.

:param attribute: attribute to check
:type attribute: str
:param default_from_name: define default environnement value from attribute name
PREFIX_ENV_VARIABLE_<upper case attribute>
:type default_from_name: bool
:return: environnement variable used
:rtype: str
"""
settings_env_variable: dict[str, Any] = asdict(self)
env_variable = settings_env_variable.get(attribute, "")
if not env_variable and default_from_name:
env_variable: str = f"{PREFIX_ENV_VARIABLE}{attribute}".upper()
return env_variable


@dataclass
class PlgSettingsStructure:
"""Plugin settings structure and defaults values."""
Expand Down Expand Up @@ -90,6 +115,7 @@ def get_plg_settings() -> PlgSettingsStructure:
"""
# get dataclass fields definition
settings_fields = fields(PlgSettingsStructure)
env_variable_settings = PlgEnvVariableSettings()

# retrieve settings from QGIS/Qt
settings = QgsSettings()
Expand All @@ -98,9 +124,17 @@ def get_plg_settings() -> PlgSettingsStructure:
# map settings values to preferences object
li_settings_values = []
for i in settings_fields:
li_settings_values.append(
settings.value(key=i.name, defaultValue=i.default, type=i.type)
)
try:
value = settings.value(key=i.name, defaultValue=i.default, type=i.type)
# If environnement variable used, get value from environnement variable
env_variable = env_variable_settings.env_variable_used(i.name)
if env_variable:
value = EnvVarParser.get_env_var(env_variable, value)
li_settings_values.append(value)
except TypeError:
li_settings_values.append(
settings.value(key=i.name, defaultValue=i.default)
)

# instanciate new settings object
options = PlgSettingsStructure(*li_settings_values)
Expand Down
44 changes: 44 additions & 0 deletions scripts/generate_docs_settings_env_vars_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#! python3 # noqa E265

"""Generate documentation table of settings and environment variables."""

# standard
import sys
from dataclasses import asdict
from pathlib import Path

# move into project package
sys.path.insert(0, f"{Path(__file__).parent.parent.resolve()}")


# plugin
from qtribu.toolbelt.preferences import PREFIX_ENV_VARIABLE, PlgSettingsStructure

# -- variables --
output_md_file: Path = Path(__file__).parent.parent.joinpath(
"docs/static/_autogenerated/settings_env_vars_table.md"
)

# -- main --

# header of the table
output_md_file.write_text(
"| Paramètre | Variable d'environnement | Valeur par défaut |\n"
"| :-------- | :----------------------: | :---------------: |\n",
encoding="utf-8",
)

print(asdict(PlgSettingsStructure()))


out_markdown: str = ""
for k, v in asdict(PlgSettingsStructure()).items():
env_var_name: str = f"{PREFIX_ENV_VARIABLE}{k}".upper()
default_value: str = repr(v).replace(
"|", "\\|"
) # escape pipe char for markdown table

out_markdown += f"| {k} | `{env_var_name}` | {default_value} |\n"

with output_md_file.open("a", encoding="utf-8") as md_file:
md_file.write(out_markdown)
76 changes: 76 additions & 0 deletions tests/qgis/test_env_var_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os
import unittest

from qtribu.toolbelt.env_var_parser import EnvVarParser


class TestEnvVarParser(unittest.TestCase):
"""Unit tests for EnvVarParser."""

def setUp(self) -> None:
"""Prepare the test environment before each test."""
self.env_backup = dict(os.environ) # Backup environment variables

def tearDown(self) -> None:
"""Restore the original environment variables after each test."""
os.environ.clear()
os.environ.update(self.env_backup)

def test_int_conversion(self) -> None:
"""Test integer conversion from environment variable"""
os.environ["MY_INT"] = "42"
self.assertEqual(EnvVarParser.get_env_var("MY_INT", 0), 42)

def test_float_conversion(self) -> None:
"""Test float conversion from environment variable"""
os.environ["MY_FLOAT"] = "3.14"
self.assertEqual(EnvVarParser.get_env_var("MY_FLOAT", 0.0), 3.14)

def test_bool_conversion_true(self) -> None:
"""Test bool conversion from environment variable"""
for true_value in ["1", "true", "yes", "on"]:
os.environ["MY_BOOL"] = true_value
self.assertTrue(EnvVarParser.get_env_var("MY_BOOL", False))

def test_bool_conversion_false(self) -> None:
"""Test bool conversion from environment variable"""
for false_value in ["0", "false", "no", "off"]:
os.environ["MY_BOOL"] = false_value
self.assertFalse(EnvVarParser.get_env_var("MY_BOOL", True))

def test_bool_invalid_defaults_to_original(self) -> None:
"""Test invalid bool conversion from environment variable"""
os.environ["MY_BOOL"] = "maybe"
self.assertFalse(
EnvVarParser.get_env_var("MY_BOOL", False)
) # Default should remain

def test_string_conversion(self) -> None:
"""Test string conversion from environment variable"""
os.environ["MY_STRING"] = "Hello, World!"
self.assertEqual(
EnvVarParser.get_env_var("MY_STRING", "default"), "Hello, World!"
)

def test_default_value_when_env_missing(self) -> None:
"""Test default value is used when environment variable is missing"""
self.assertEqual(EnvVarParser.get_env_var("MISSING_INT", 99), 99)

def test_invalid_int_fallback_to_default(self) -> None:
"""Test default value used when the environment variable is not a valid int"""
os.environ["MY_INT"] = "not_an_int"
self.assertEqual(EnvVarParser.get_env_var("MY_INT", 10), 10)

def test_invalid_float_fallback_to_default(self) -> None:
"""Test default value used when the environment variable is not a valid float"""
os.environ["MY_FLOAT"] = "not_a_float"
self.assertEqual(EnvVarParser.get_env_var("MY_FLOAT", 1.23), 1.23)

def test_unsupported_type(self) -> None:
"""Test exception is raised when the type expected is not supported"""
os.environ["INT_LIST"] = "1,2,3,4"
self.assertRaises(TypeError, EnvVarParser.get_env_var, "INT_LIST", [1, 2, 3, 4])


if __name__ == "__main__":
unittest.main()
Loading