mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #1649 from YunoHost/before-pydantic
ConfigPanel: Before pydantic (renames) 1/3
This commit is contained in:
commit
76bf9044c4
6 changed files with 777 additions and 739 deletions
26
src/app.py
26
src/app.py
|
@ -50,8 +50,8 @@ from moulinette.utils.filesystem import (
|
||||||
|
|
||||||
from yunohost.utils.configpanel import ConfigPanel, ask_questions_and_parse_answers
|
from yunohost.utils.configpanel import ConfigPanel, ask_questions_and_parse_answers
|
||||||
from yunohost.utils.form import (
|
from yunohost.utils.form import (
|
||||||
DomainQuestion,
|
DomainOption,
|
||||||
PathQuestion,
|
WebPathOption,
|
||||||
hydrate_questions_with_choices,
|
hydrate_questions_with_choices,
|
||||||
)
|
)
|
||||||
from yunohost.utils.i18n import _value_for_locale
|
from yunohost.utils.i18n import _value_for_locale
|
||||||
|
@ -430,10 +430,10 @@ def app_change_url(operation_logger, app, domain, path):
|
||||||
|
|
||||||
# Normalize path and domain format
|
# Normalize path and domain format
|
||||||
|
|
||||||
domain = DomainQuestion.normalize(domain)
|
domain = DomainOption.normalize(domain)
|
||||||
old_domain = DomainQuestion.normalize(old_domain)
|
old_domain = DomainOption.normalize(old_domain)
|
||||||
path = PathQuestion.normalize(path)
|
path = WebPathOption.normalize(path)
|
||||||
old_path = PathQuestion.normalize(old_path)
|
old_path = WebPathOption.normalize(old_path)
|
||||||
|
|
||||||
if (domain, path) == (old_domain, old_path):
|
if (domain, path) == (old_domain, old_path):
|
||||||
raise YunohostValidationError(
|
raise YunohostValidationError(
|
||||||
|
@ -1660,8 +1660,8 @@ def app_register_url(app, domain, path):
|
||||||
permission_sync_to_user,
|
permission_sync_to_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
domain = DomainQuestion.normalize(domain)
|
domain = DomainOption.normalize(domain)
|
||||||
path = PathQuestion.normalize(path)
|
path = WebPathOption.normalize(path)
|
||||||
|
|
||||||
# We cannot change the url of an app already installed simply by changing
|
# We cannot change the url of an app already installed simply by changing
|
||||||
# the settings...
|
# the settings...
|
||||||
|
@ -1878,13 +1878,13 @@ class AppConfigPanel(ConfigPanel):
|
||||||
save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml")
|
save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml")
|
||||||
config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml")
|
config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml")
|
||||||
|
|
||||||
def _load_current_values(self):
|
|
||||||
self.values = self._call_config_script("show")
|
|
||||||
|
|
||||||
def _run_action(self, action):
|
def _run_action(self, action):
|
||||||
env = {key: str(value) for key, value in self.new_values.items()}
|
env = {key: str(value) for key, value in self.new_values.items()}
|
||||||
self._call_config_script(action, env=env)
|
self._call_config_script(action, env=env)
|
||||||
|
|
||||||
|
def _get_raw_settings(self):
|
||||||
|
self.values = self._call_config_script("show")
|
||||||
|
|
||||||
def _apply(self):
|
def _apply(self):
|
||||||
env = {key: str(value) for key, value in self.new_values.items()}
|
env = {key: str(value) for key, value in self.new_values.items()}
|
||||||
return_content = self._call_config_script("apply", env=env)
|
return_content = self._call_config_script("apply", env=env)
|
||||||
|
@ -2853,8 +2853,8 @@ def _get_conflicting_apps(domain, path, ignore_app=None):
|
||||||
|
|
||||||
from yunohost.domain import _assert_domain_exists
|
from yunohost.domain import _assert_domain_exists
|
||||||
|
|
||||||
domain = DomainQuestion.normalize(domain)
|
domain = DomainOption.normalize(domain)
|
||||||
path = PathQuestion.normalize(path)
|
path = WebPathOption.normalize(path)
|
||||||
|
|
||||||
# Abort if domain is unknown
|
# Abort if domain is unknown
|
||||||
_assert_domain_exists(domain)
|
_assert_domain_exists(domain)
|
||||||
|
|
158
src/domain.py
158
src/domain.py
|
@ -34,7 +34,7 @@ from yunohost.app import (
|
||||||
)
|
)
|
||||||
from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf
|
from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf
|
||||||
from yunohost.utils.configpanel import ConfigPanel
|
from yunohost.utils.configpanel import ConfigPanel
|
||||||
from yunohost.utils.form import Question
|
from yunohost.utils.form import BaseOption
|
||||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||||
from yunohost.log import is_unit_operation
|
from yunohost.log import is_unit_operation
|
||||||
|
|
||||||
|
@ -528,7 +528,7 @@ def domain_config_set(
|
||||||
"""
|
"""
|
||||||
Apply a new domain configuration
|
Apply a new domain configuration
|
||||||
"""
|
"""
|
||||||
Question.operation_logger = operation_logger
|
BaseOption.operation_logger = operation_logger
|
||||||
config = DomainConfigPanel(domain)
|
config = DomainConfigPanel(domain)
|
||||||
return config.set(key, value, args, args_file, operation_logger=operation_logger)
|
return config.set(key, value, args, args_file, operation_logger=operation_logger)
|
||||||
|
|
||||||
|
@ -538,6 +538,83 @@ class DomainConfigPanel(ConfigPanel):
|
||||||
save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml"
|
save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml"
|
||||||
save_mode = "diff"
|
save_mode = "diff"
|
||||||
|
|
||||||
|
def get(self, key="", mode="classic"):
|
||||||
|
result = super().get(key=key, mode=mode)
|
||||||
|
|
||||||
|
if mode == "full":
|
||||||
|
for panel, section, option in self._iterate():
|
||||||
|
# This injects:
|
||||||
|
# i18n: domain_config_cert_renew_help
|
||||||
|
# i18n: domain_config_default_app_help
|
||||||
|
# i18n: domain_config_xmpp_help
|
||||||
|
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
|
||||||
|
option["help"] = m18n.n(
|
||||||
|
self.config["i18n"] + "_" + option["id"] + "_help"
|
||||||
|
)
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_raw_config(self):
|
||||||
|
toml = super()._get_raw_config()
|
||||||
|
|
||||||
|
toml["feature"]["xmpp"]["xmpp"]["default"] = (
|
||||||
|
1 if self.entity == _get_maindomain() else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optimize wether or not to load the DNS section,
|
||||||
|
# e.g. we don't want to trigger the whole _get_registary_config_section
|
||||||
|
# when just getting the current value from the feature section
|
||||||
|
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
|
||||||
|
if not filter_key or filter_key[0] == "dns":
|
||||||
|
from yunohost.dns import _get_registrar_config_section
|
||||||
|
|
||||||
|
toml["dns"]["registrar"] = _get_registrar_config_section(self.entity)
|
||||||
|
|
||||||
|
# FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ...
|
||||||
|
self.registar_id = toml["dns"]["registrar"]["registrar"]["value"]
|
||||||
|
del toml["dns"]["registrar"]["registrar"]["value"]
|
||||||
|
|
||||||
|
# Cert stuff
|
||||||
|
if not filter_key or filter_key[0] == "cert":
|
||||||
|
from yunohost.certificate import certificate_status
|
||||||
|
|
||||||
|
status = certificate_status([self.entity], full=True)["certificates"][
|
||||||
|
self.entity
|
||||||
|
]
|
||||||
|
|
||||||
|
toml["cert"]["cert"]["cert_summary"]["style"] = status["style"]
|
||||||
|
|
||||||
|
# i18n: domain_config_cert_summary_expired
|
||||||
|
# i18n: domain_config_cert_summary_selfsigned
|
||||||
|
# i18n: domain_config_cert_summary_abouttoexpire
|
||||||
|
# i18n: domain_config_cert_summary_ok
|
||||||
|
# i18n: domain_config_cert_summary_letsencrypt
|
||||||
|
toml["cert"]["cert"]["cert_summary"]["ask"] = m18n.n(
|
||||||
|
f"domain_config_cert_summary_{status['summary']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ...
|
||||||
|
self.cert_status = status
|
||||||
|
|
||||||
|
return toml
|
||||||
|
|
||||||
|
def _get_raw_settings(self):
|
||||||
|
# TODO add mechanism to share some settings with other domains on the same zone
|
||||||
|
super()._get_raw_settings()
|
||||||
|
|
||||||
|
# FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ...
|
||||||
|
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
|
||||||
|
if not filter_key or filter_key[0] == "dns":
|
||||||
|
self.values["registrar"] = self.registar_id
|
||||||
|
|
||||||
|
# FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ...
|
||||||
|
if not filter_key or filter_key[0] == "cert":
|
||||||
|
self.values["cert_validity"] = self.cert_status["validity"]
|
||||||
|
self.values["cert_issuer"] = self.cert_status["CA_type"]
|
||||||
|
self.values["acme_eligible"] = self.cert_status["ACME_eligible"]
|
||||||
|
self.values["summary"] = self.cert_status["summary"]
|
||||||
|
|
||||||
def _apply(self):
|
def _apply(self):
|
||||||
if (
|
if (
|
||||||
"default_app" in self.future_values
|
"default_app" in self.future_values
|
||||||
|
@ -586,83 +663,6 @@ class DomainConfigPanel(ConfigPanel):
|
||||||
if stuff_to_regen_conf:
|
if stuff_to_regen_conf:
|
||||||
regen_conf(names=stuff_to_regen_conf)
|
regen_conf(names=stuff_to_regen_conf)
|
||||||
|
|
||||||
def _get_toml(self):
|
|
||||||
toml = super()._get_toml()
|
|
||||||
|
|
||||||
toml["feature"]["xmpp"]["xmpp"]["default"] = (
|
|
||||||
1 if self.entity == _get_maindomain() else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Optimize wether or not to load the DNS section,
|
|
||||||
# e.g. we don't want to trigger the whole _get_registary_config_section
|
|
||||||
# when just getting the current value from the feature section
|
|
||||||
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
|
|
||||||
if not filter_key or filter_key[0] == "dns":
|
|
||||||
from yunohost.dns import _get_registrar_config_section
|
|
||||||
|
|
||||||
toml["dns"]["registrar"] = _get_registrar_config_section(self.entity)
|
|
||||||
|
|
||||||
# FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ...
|
|
||||||
self.registar_id = toml["dns"]["registrar"]["registrar"]["value"]
|
|
||||||
del toml["dns"]["registrar"]["registrar"]["value"]
|
|
||||||
|
|
||||||
# Cert stuff
|
|
||||||
if not filter_key or filter_key[0] == "cert":
|
|
||||||
from yunohost.certificate import certificate_status
|
|
||||||
|
|
||||||
status = certificate_status([self.entity], full=True)["certificates"][
|
|
||||||
self.entity
|
|
||||||
]
|
|
||||||
|
|
||||||
toml["cert"]["cert"]["cert_summary"]["style"] = status["style"]
|
|
||||||
|
|
||||||
# i18n: domain_config_cert_summary_expired
|
|
||||||
# i18n: domain_config_cert_summary_selfsigned
|
|
||||||
# i18n: domain_config_cert_summary_abouttoexpire
|
|
||||||
# i18n: domain_config_cert_summary_ok
|
|
||||||
# i18n: domain_config_cert_summary_letsencrypt
|
|
||||||
toml["cert"]["cert"]["cert_summary"]["ask"] = m18n.n(
|
|
||||||
f"domain_config_cert_summary_{status['summary']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ...
|
|
||||||
self.cert_status = status
|
|
||||||
|
|
||||||
return toml
|
|
||||||
|
|
||||||
def get(self, key="", mode="classic"):
|
|
||||||
result = super().get(key=key, mode=mode)
|
|
||||||
|
|
||||||
if mode == "full":
|
|
||||||
for panel, section, option in self._iterate():
|
|
||||||
# This injects:
|
|
||||||
# i18n: domain_config_cert_renew_help
|
|
||||||
# i18n: domain_config_default_app_help
|
|
||||||
# i18n: domain_config_xmpp_help
|
|
||||||
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
|
|
||||||
option["help"] = m18n.n(
|
|
||||||
self.config["i18n"] + "_" + option["id"] + "_help"
|
|
||||||
)
|
|
||||||
return self.config
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _load_current_values(self):
|
|
||||||
# TODO add mechanism to share some settings with other domains on the same zone
|
|
||||||
super()._load_current_values()
|
|
||||||
|
|
||||||
# FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ...
|
|
||||||
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
|
|
||||||
if not filter_key or filter_key[0] == "dns":
|
|
||||||
self.values["registrar"] = self.registar_id
|
|
||||||
|
|
||||||
# FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ...
|
|
||||||
if not filter_key or filter_key[0] == "cert":
|
|
||||||
self.values["cert_validity"] = self.cert_status["validity"]
|
|
||||||
self.values["cert_issuer"] = self.cert_status["CA_type"]
|
|
||||||
self.values["acme_eligible"] = self.cert_status["ACME_eligible"]
|
|
||||||
self.values["summary"] = self.cert_status["summary"]
|
|
||||||
|
|
||||||
|
|
||||||
def domain_action_run(domain, action, args=None):
|
def domain_action_run(domain, action, args=None):
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
178
src/settings.py
178
src/settings.py
|
@ -22,7 +22,7 @@ import subprocess
|
||||||
from moulinette import m18n
|
from moulinette import m18n
|
||||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||||
from yunohost.utils.configpanel import ConfigPanel
|
from yunohost.utils.configpanel import ConfigPanel
|
||||||
from yunohost.utils.form import Question
|
from yunohost.utils.form import BaseOption
|
||||||
from moulinette.utils.log import getActionLogger
|
from moulinette.utils.log import getActionLogger
|
||||||
from yunohost.regenconf import regen_conf
|
from yunohost.regenconf import regen_conf
|
||||||
from yunohost.firewall import firewall_reload
|
from yunohost.firewall import firewall_reload
|
||||||
|
@ -82,7 +82,7 @@ def settings_set(operation_logger, key=None, value=None, args=None, args_file=No
|
||||||
value -- New value
|
value -- New value
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Question.operation_logger = operation_logger
|
BaseOption.operation_logger = operation_logger
|
||||||
settings = SettingsConfigPanel()
|
settings = SettingsConfigPanel()
|
||||||
key = translate_legacy_settings_to_configpanel_settings(key)
|
key = translate_legacy_settings_to_configpanel_settings(key)
|
||||||
return settings.set(key, value, args, args_file, operation_logger=operation_logger)
|
return settings.set(key, value, args, args_file, operation_logger=operation_logger)
|
||||||
|
@ -125,6 +125,93 @@ class SettingsConfigPanel(ConfigPanel):
|
||||||
def __init__(self, config_path=None, save_path=None, creation=False):
|
def __init__(self, config_path=None, save_path=None, creation=False):
|
||||||
super().__init__("settings")
|
super().__init__("settings")
|
||||||
|
|
||||||
|
def get(self, key="", mode="classic"):
|
||||||
|
result = super().get(key=key, mode=mode)
|
||||||
|
|
||||||
|
if mode == "full":
|
||||||
|
for panel, section, option in self._iterate():
|
||||||
|
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
|
||||||
|
option["help"] = m18n.n(
|
||||||
|
self.config["i18n"] + "_" + option["id"] + "_help"
|
||||||
|
)
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
# Dirty hack to let settings_get() to work from a python script
|
||||||
|
if isinstance(result, str) and result in ["True", "False"]:
|
||||||
|
result = bool(result == "True")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def reset(self, key="", operation_logger=None):
|
||||||
|
self.filter_key = key
|
||||||
|
|
||||||
|
# Read config panel toml
|
||||||
|
self._get_config_panel()
|
||||||
|
|
||||||
|
if not self.config:
|
||||||
|
raise YunohostValidationError("config_no_panel")
|
||||||
|
|
||||||
|
# Replace all values with default values
|
||||||
|
self.values = self._get_default_values()
|
||||||
|
|
||||||
|
BaseOption.operation_logger = operation_logger
|
||||||
|
|
||||||
|
if operation_logger:
|
||||||
|
operation_logger.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._apply()
|
||||||
|
except YunohostError:
|
||||||
|
raise
|
||||||
|
# Script got manually interrupted ...
|
||||||
|
# N.B. : KeyboardInterrupt does not inherit from Exception
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
error = m18n.n("operation_interrupted")
|
||||||
|
logger.error(m18n.n("config_apply_failed", error=error))
|
||||||
|
raise
|
||||||
|
# Something wrong happened in Yunohost's code (most probably hook_exec)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
|
||||||
|
logger.error(m18n.n("config_apply_failed", error=error))
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.success(m18n.n("global_settings_reset_success"))
|
||||||
|
operation_logger.success()
|
||||||
|
|
||||||
|
def _get_raw_config(self):
|
||||||
|
toml = super()._get_raw_config()
|
||||||
|
|
||||||
|
# Dynamic choice list for portal themes
|
||||||
|
THEMEDIR = "/usr/share/ssowat/portal/assets/themes/"
|
||||||
|
try:
|
||||||
|
themes = [d for d in os.listdir(THEMEDIR) if os.path.isdir(THEMEDIR + d)]
|
||||||
|
except Exception:
|
||||||
|
themes = ["unsplash", "vapor", "light", "default", "clouds"]
|
||||||
|
toml["misc"]["portal"]["portal_theme"]["choices"] = themes
|
||||||
|
|
||||||
|
return toml
|
||||||
|
|
||||||
|
def _get_raw_settings(self):
|
||||||
|
super()._get_raw_settings()
|
||||||
|
|
||||||
|
# Specific logic for those settings who are "virtual" settings
|
||||||
|
# and only meant to have a custom setter mapped to tools_rootpw
|
||||||
|
self.values["root_password"] = ""
|
||||||
|
self.values["root_password_confirm"] = ""
|
||||||
|
|
||||||
|
# Specific logic for virtual setting "passwordless_sudo"
|
||||||
|
try:
|
||||||
|
from yunohost.utils.ldap import _get_ldap_interface
|
||||||
|
|
||||||
|
ldap = _get_ldap_interface()
|
||||||
|
self.values["passwordless_sudo"] = "!authenticate" in ldap.search(
|
||||||
|
"ou=sudo", "cn=admins", ["sudoOption"]
|
||||||
|
)[0].get("sudoOption", [])
|
||||||
|
except Exception:
|
||||||
|
self.values["passwordless_sudo"] = False
|
||||||
|
|
||||||
def _apply(self):
|
def _apply(self):
|
||||||
root_password = self.new_values.pop("root_password", None)
|
root_password = self.new_values.pop("root_password", None)
|
||||||
root_password_confirm = self.new_values.pop("root_password_confirm", None)
|
root_password_confirm = self.new_values.pop("root_password_confirm", None)
|
||||||
|
@ -170,93 +257,6 @@ class SettingsConfigPanel(ConfigPanel):
|
||||||
logger.error(f"Post-change hook for setting failed : {e}")
|
logger.error(f"Post-change hook for setting failed : {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _get_toml(self):
|
|
||||||
toml = super()._get_toml()
|
|
||||||
|
|
||||||
# Dynamic choice list for portal themes
|
|
||||||
THEMEDIR = "/usr/share/ssowat/portal/assets/themes/"
|
|
||||||
try:
|
|
||||||
themes = [d for d in os.listdir(THEMEDIR) if os.path.isdir(THEMEDIR + d)]
|
|
||||||
except Exception:
|
|
||||||
themes = ["unsplash", "vapor", "light", "default", "clouds"]
|
|
||||||
toml["misc"]["portal"]["portal_theme"]["choices"] = themes
|
|
||||||
|
|
||||||
return toml
|
|
||||||
|
|
||||||
def _load_current_values(self):
|
|
||||||
super()._load_current_values()
|
|
||||||
|
|
||||||
# Specific logic for those settings who are "virtual" settings
|
|
||||||
# and only meant to have a custom setter mapped to tools_rootpw
|
|
||||||
self.values["root_password"] = ""
|
|
||||||
self.values["root_password_confirm"] = ""
|
|
||||||
|
|
||||||
# Specific logic for virtual setting "passwordless_sudo"
|
|
||||||
try:
|
|
||||||
from yunohost.utils.ldap import _get_ldap_interface
|
|
||||||
|
|
||||||
ldap = _get_ldap_interface()
|
|
||||||
self.values["passwordless_sudo"] = "!authenticate" in ldap.search(
|
|
||||||
"ou=sudo", "cn=admins", ["sudoOption"]
|
|
||||||
)[0].get("sudoOption", [])
|
|
||||||
except Exception:
|
|
||||||
self.values["passwordless_sudo"] = False
|
|
||||||
|
|
||||||
def get(self, key="", mode="classic"):
|
|
||||||
result = super().get(key=key, mode=mode)
|
|
||||||
|
|
||||||
if mode == "full":
|
|
||||||
for panel, section, option in self._iterate():
|
|
||||||
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
|
|
||||||
option["help"] = m18n.n(
|
|
||||||
self.config["i18n"] + "_" + option["id"] + "_help"
|
|
||||||
)
|
|
||||||
return self.config
|
|
||||||
|
|
||||||
# Dirty hack to let settings_get() to work from a python script
|
|
||||||
if isinstance(result, str) and result in ["True", "False"]:
|
|
||||||
result = bool(result == "True")
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def reset(self, key="", operation_logger=None):
|
|
||||||
self.filter_key = key
|
|
||||||
|
|
||||||
# Read config panel toml
|
|
||||||
self._get_config_panel()
|
|
||||||
|
|
||||||
if not self.config:
|
|
||||||
raise YunohostValidationError("config_no_panel")
|
|
||||||
|
|
||||||
# Replace all values with default values
|
|
||||||
self.values = self._get_default_values()
|
|
||||||
|
|
||||||
Question.operation_logger = operation_logger
|
|
||||||
|
|
||||||
if operation_logger:
|
|
||||||
operation_logger.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._apply()
|
|
||||||
except YunohostError:
|
|
||||||
raise
|
|
||||||
# Script got manually interrupted ...
|
|
||||||
# N.B. : KeyboardInterrupt does not inherit from Exception
|
|
||||||
except (KeyboardInterrupt, EOFError):
|
|
||||||
error = m18n.n("operation_interrupted")
|
|
||||||
logger.error(m18n.n("config_apply_failed", error=error))
|
|
||||||
raise
|
|
||||||
# Something wrong happened in Yunohost's code (most probably hook_exec)
|
|
||||||
except Exception:
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
|
|
||||||
logger.error(m18n.n("config_apply_failed", error=error))
|
|
||||||
raise
|
|
||||||
|
|
||||||
logger.success(m18n.n("global_settings_reset_success"))
|
|
||||||
operation_logger.success()
|
|
||||||
|
|
||||||
|
|
||||||
# Meant to be a dict of setting_name -> function to call
|
# Meant to be a dict of setting_name -> function to call
|
||||||
post_change_hooks = {}
|
post_change_hooks = {}
|
||||||
|
|
|
@ -15,14 +15,14 @@ from _pytest.mark.structures import ParameterSet
|
||||||
from moulinette import Moulinette
|
from moulinette import Moulinette
|
||||||
from yunohost import app, domain, user
|
from yunohost import app, domain, user
|
||||||
from yunohost.utils.form import (
|
from yunohost.utils.form import (
|
||||||
ARGUMENTS_TYPE_PARSERS,
|
OPTIONS,
|
||||||
ask_questions_and_parse_answers,
|
ask_questions_and_parse_answers,
|
||||||
DisplayTextQuestion,
|
DisplayTextOption,
|
||||||
PasswordQuestion,
|
PasswordOption,
|
||||||
DomainQuestion,
|
DomainOption,
|
||||||
PathQuestion,
|
WebPathOption,
|
||||||
BooleanQuestion,
|
BooleanOption,
|
||||||
FileQuestion,
|
FileOption,
|
||||||
evaluate_simple_js_expression,
|
evaluate_simple_js_expression,
|
||||||
)
|
)
|
||||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||||
|
@ -438,9 +438,9 @@ class BaseTest:
|
||||||
id_ = raw_option["id"]
|
id_ = raw_option["id"]
|
||||||
option, value = _fill_or_prompt_one_option(raw_option, None)
|
option, value = _fill_or_prompt_one_option(raw_option, None)
|
||||||
|
|
||||||
is_special_readonly_option = isinstance(option, DisplayTextQuestion)
|
is_special_readonly_option = isinstance(option, DisplayTextOption)
|
||||||
|
|
||||||
assert isinstance(option, ARGUMENTS_TYPE_PARSERS[raw_option["type"]])
|
assert isinstance(option, OPTIONS[raw_option["type"]])
|
||||||
assert option.type == raw_option["type"]
|
assert option.type == raw_option["type"]
|
||||||
assert option.name == id_
|
assert option.name == id_
|
||||||
assert option.ask == {"en": id_}
|
assert option.ask == {"en": id_}
|
||||||
|
@ -734,7 +734,7 @@ class TestPassword(BaseTest):
|
||||||
], reason="Should output exactly the same"),
|
], reason="Should output exactly the same"),
|
||||||
("s3cr3t!!", "s3cr3t!!"),
|
("s3cr3t!!", "s3cr3t!!"),
|
||||||
("secret", FAIL),
|
("secret", FAIL),
|
||||||
*[("supersecret" + char, FAIL) for char in PasswordQuestion.forbidden_chars], # FIXME maybe add ` \n` to the list?
|
*[("supersecret" + char, FAIL) for char in PasswordOption.forbidden_chars], # FIXME maybe add ` \n` to the list?
|
||||||
# readonly
|
# readonly
|
||||||
*xpass(scenarios=[
|
*xpass(scenarios=[
|
||||||
("s3cr3t!!", "s3cr3t!!", {"readonly": True}),
|
("s3cr3t!!", "s3cr3t!!", {"readonly": True}),
|
||||||
|
@ -1225,9 +1225,9 @@ class TestUrl(BaseTest):
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def file_clean():
|
def file_clean():
|
||||||
FileQuestion.clean_upload_dirs()
|
FileOption.clean_upload_dirs()
|
||||||
yield
|
yield
|
||||||
FileQuestion.clean_upload_dirs()
|
FileOption.clean_upload_dirs()
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
@ -1263,7 +1263,7 @@ def _test_file_intake_may_fail(raw_option, intake, expected_output):
|
||||||
with open(value) as f:
|
with open(value) as f:
|
||||||
assert f.read() == expected_output
|
assert f.read() == expected_output
|
||||||
|
|
||||||
FileQuestion.clean_upload_dirs()
|
FileOption.clean_upload_dirs()
|
||||||
|
|
||||||
assert not os.path.exists(value)
|
assert not os.path.exists(value)
|
||||||
|
|
||||||
|
@ -2138,88 +2138,88 @@ def test_question_number_input_test_ask_with_example():
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_boolean_nominal():
|
def test_normalize_boolean_nominal():
|
||||||
assert BooleanQuestion.normalize("yes") == 1
|
assert BooleanOption.normalize("yes") == 1
|
||||||
assert BooleanQuestion.normalize("Yes") == 1
|
assert BooleanOption.normalize("Yes") == 1
|
||||||
assert BooleanQuestion.normalize(" yes ") == 1
|
assert BooleanOption.normalize(" yes ") == 1
|
||||||
assert BooleanQuestion.normalize("y") == 1
|
assert BooleanOption.normalize("y") == 1
|
||||||
assert BooleanQuestion.normalize("true") == 1
|
assert BooleanOption.normalize("true") == 1
|
||||||
assert BooleanQuestion.normalize("True") == 1
|
assert BooleanOption.normalize("True") == 1
|
||||||
assert BooleanQuestion.normalize("on") == 1
|
assert BooleanOption.normalize("on") == 1
|
||||||
assert BooleanQuestion.normalize("1") == 1
|
assert BooleanOption.normalize("1") == 1
|
||||||
assert BooleanQuestion.normalize(1) == 1
|
assert BooleanOption.normalize(1) == 1
|
||||||
|
|
||||||
assert BooleanQuestion.normalize("no") == 0
|
assert BooleanOption.normalize("no") == 0
|
||||||
assert BooleanQuestion.normalize("No") == 0
|
assert BooleanOption.normalize("No") == 0
|
||||||
assert BooleanQuestion.normalize(" no ") == 0
|
assert BooleanOption.normalize(" no ") == 0
|
||||||
assert BooleanQuestion.normalize("n") == 0
|
assert BooleanOption.normalize("n") == 0
|
||||||
assert BooleanQuestion.normalize("false") == 0
|
assert BooleanOption.normalize("false") == 0
|
||||||
assert BooleanQuestion.normalize("False") == 0
|
assert BooleanOption.normalize("False") == 0
|
||||||
assert BooleanQuestion.normalize("off") == 0
|
assert BooleanOption.normalize("off") == 0
|
||||||
assert BooleanQuestion.normalize("0") == 0
|
assert BooleanOption.normalize("0") == 0
|
||||||
assert BooleanQuestion.normalize(0) == 0
|
assert BooleanOption.normalize(0) == 0
|
||||||
|
|
||||||
assert BooleanQuestion.normalize("") is None
|
assert BooleanOption.normalize("") is None
|
||||||
assert BooleanQuestion.normalize(" ") is None
|
assert BooleanOption.normalize(" ") is None
|
||||||
assert BooleanQuestion.normalize(" none ") is None
|
assert BooleanOption.normalize(" none ") is None
|
||||||
assert BooleanQuestion.normalize("None") is None
|
assert BooleanOption.normalize("None") is None
|
||||||
assert BooleanQuestion.normalize("noNe") is None
|
assert BooleanOption.normalize("noNe") is None
|
||||||
assert BooleanQuestion.normalize(None) is None
|
assert BooleanOption.normalize(None) is None
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_boolean_humanize():
|
def test_normalize_boolean_humanize():
|
||||||
assert BooleanQuestion.humanize("yes") == "yes"
|
assert BooleanOption.humanize("yes") == "yes"
|
||||||
assert BooleanQuestion.humanize("true") == "yes"
|
assert BooleanOption.humanize("true") == "yes"
|
||||||
assert BooleanQuestion.humanize("on") == "yes"
|
assert BooleanOption.humanize("on") == "yes"
|
||||||
|
|
||||||
assert BooleanQuestion.humanize("no") == "no"
|
assert BooleanOption.humanize("no") == "no"
|
||||||
assert BooleanQuestion.humanize("false") == "no"
|
assert BooleanOption.humanize("false") == "no"
|
||||||
assert BooleanQuestion.humanize("off") == "no"
|
assert BooleanOption.humanize("off") == "no"
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_boolean_invalid():
|
def test_normalize_boolean_invalid():
|
||||||
with pytest.raises(YunohostValidationError):
|
with pytest.raises(YunohostValidationError):
|
||||||
BooleanQuestion.normalize("yesno")
|
BooleanOption.normalize("yesno")
|
||||||
with pytest.raises(YunohostValidationError):
|
with pytest.raises(YunohostValidationError):
|
||||||
BooleanQuestion.normalize("foobar")
|
BooleanOption.normalize("foobar")
|
||||||
with pytest.raises(YunohostValidationError):
|
with pytest.raises(YunohostValidationError):
|
||||||
BooleanQuestion.normalize("enabled")
|
BooleanOption.normalize("enabled")
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_boolean_special_yesno():
|
def test_normalize_boolean_special_yesno():
|
||||||
customyesno = {"yes": "enabled", "no": "disabled"}
|
customyesno = {"yes": "enabled", "no": "disabled"}
|
||||||
|
|
||||||
assert BooleanQuestion.normalize("yes", customyesno) == "enabled"
|
assert BooleanOption.normalize("yes", customyesno) == "enabled"
|
||||||
assert BooleanQuestion.normalize("true", customyesno) == "enabled"
|
assert BooleanOption.normalize("true", customyesno) == "enabled"
|
||||||
assert BooleanQuestion.normalize("enabled", customyesno) == "enabled"
|
assert BooleanOption.normalize("enabled", customyesno) == "enabled"
|
||||||
assert BooleanQuestion.humanize("yes", customyesno) == "yes"
|
assert BooleanOption.humanize("yes", customyesno) == "yes"
|
||||||
assert BooleanQuestion.humanize("true", customyesno) == "yes"
|
assert BooleanOption.humanize("true", customyesno) == "yes"
|
||||||
assert BooleanQuestion.humanize("enabled", customyesno) == "yes"
|
assert BooleanOption.humanize("enabled", customyesno) == "yes"
|
||||||
|
|
||||||
assert BooleanQuestion.normalize("no", customyesno) == "disabled"
|
assert BooleanOption.normalize("no", customyesno) == "disabled"
|
||||||
assert BooleanQuestion.normalize("false", customyesno) == "disabled"
|
assert BooleanOption.normalize("false", customyesno) == "disabled"
|
||||||
assert BooleanQuestion.normalize("disabled", customyesno) == "disabled"
|
assert BooleanOption.normalize("disabled", customyesno) == "disabled"
|
||||||
assert BooleanQuestion.humanize("no", customyesno) == "no"
|
assert BooleanOption.humanize("no", customyesno) == "no"
|
||||||
assert BooleanQuestion.humanize("false", customyesno) == "no"
|
assert BooleanOption.humanize("false", customyesno) == "no"
|
||||||
assert BooleanQuestion.humanize("disabled", customyesno) == "no"
|
assert BooleanOption.humanize("disabled", customyesno) == "no"
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_domain():
|
def test_normalize_domain():
|
||||||
assert DomainQuestion.normalize("https://yolo.swag/") == "yolo.swag"
|
assert DomainOption.normalize("https://yolo.swag/") == "yolo.swag"
|
||||||
assert DomainQuestion.normalize("http://yolo.swag") == "yolo.swag"
|
assert DomainOption.normalize("http://yolo.swag") == "yolo.swag"
|
||||||
assert DomainQuestion.normalize("yolo.swag/") == "yolo.swag"
|
assert DomainOption.normalize("yolo.swag/") == "yolo.swag"
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_path():
|
def test_normalize_path():
|
||||||
assert PathQuestion.normalize("") == "/"
|
assert WebPathOption.normalize("") == "/"
|
||||||
assert PathQuestion.normalize("") == "/"
|
assert WebPathOption.normalize("") == "/"
|
||||||
assert PathQuestion.normalize("macnuggets") == "/macnuggets"
|
assert WebPathOption.normalize("macnuggets") == "/macnuggets"
|
||||||
assert PathQuestion.normalize("/macnuggets") == "/macnuggets"
|
assert WebPathOption.normalize("/macnuggets") == "/macnuggets"
|
||||||
assert PathQuestion.normalize(" /macnuggets ") == "/macnuggets"
|
assert WebPathOption.normalize(" /macnuggets ") == "/macnuggets"
|
||||||
assert PathQuestion.normalize("/macnuggets") == "/macnuggets"
|
assert WebPathOption.normalize("/macnuggets") == "/macnuggets"
|
||||||
assert PathQuestion.normalize("mac/nuggets") == "/mac/nuggets"
|
assert WebPathOption.normalize("mac/nuggets") == "/mac/nuggets"
|
||||||
assert PathQuestion.normalize("/macnuggets/") == "/macnuggets"
|
assert WebPathOption.normalize("/macnuggets/") == "/macnuggets"
|
||||||
assert PathQuestion.normalize("macnuggets/") == "/macnuggets"
|
assert WebPathOption.normalize("macnuggets/") == "/macnuggets"
|
||||||
assert PathQuestion.normalize("////macnuggets///") == "/macnuggets"
|
assert WebPathOption.normalize("////macnuggets///") == "/macnuggets"
|
||||||
|
|
||||||
|
|
||||||
def test_simple_evaluate():
|
def test_simple_evaluate():
|
||||||
|
|
|
@ -23,25 +23,19 @@ import urllib.parse
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from moulinette.interfaces.cli import colorize
|
|
||||||
from moulinette import Moulinette, m18n
|
from moulinette import Moulinette, m18n
|
||||||
|
from moulinette.interfaces.cli import colorize
|
||||||
|
from moulinette.utils.filesystem import mkdir, read_toml, read_yaml, write_to_yaml
|
||||||
from moulinette.utils.log import getActionLogger
|
from moulinette.utils.log import getActionLogger
|
||||||
from moulinette.utils.filesystem import (
|
|
||||||
read_toml,
|
|
||||||
read_yaml,
|
|
||||||
write_to_yaml,
|
|
||||||
mkdir,
|
|
||||||
)
|
|
||||||
|
|
||||||
from yunohost.utils.i18n import _value_for_locale
|
|
||||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||||
from yunohost.utils.form import (
|
from yunohost.utils.form import (
|
||||||
ARGUMENTS_TYPE_PARSERS,
|
OPTIONS,
|
||||||
FileQuestion,
|
BaseOption,
|
||||||
Question,
|
FileOption,
|
||||||
ask_questions_and_parse_answers,
|
ask_questions_and_parse_answers,
|
||||||
evaluate_simple_js_expression,
|
evaluate_simple_js_expression,
|
||||||
)
|
)
|
||||||
|
from yunohost.utils.i18n import _value_for_locale
|
||||||
|
|
||||||
logger = getActionLogger("yunohost.configpanel")
|
logger = getActionLogger("yunohost.configpanel")
|
||||||
CONFIG_PANEL_VERSION_SUPPORTED = 1.0
|
CONFIG_PANEL_VERSION_SUPPORTED = 1.0
|
||||||
|
@ -116,7 +110,7 @@ class ConfigPanel:
|
||||||
raise YunohostValidationError("config_no_panel")
|
raise YunohostValidationError("config_no_panel")
|
||||||
|
|
||||||
# Read or get values and hydrate the config
|
# Read or get values and hydrate the config
|
||||||
self._load_current_values()
|
self._get_raw_settings()
|
||||||
self._hydrate()
|
self._hydrate()
|
||||||
|
|
||||||
# In 'classic' mode, we display the current value if key refer to an option
|
# In 'classic' mode, we display the current value if key refer to an option
|
||||||
|
@ -127,7 +121,7 @@ class ConfigPanel:
|
||||||
option_type = None
|
option_type = None
|
||||||
for _, _, option_ in self._iterate():
|
for _, _, option_ in self._iterate():
|
||||||
if option_["id"] == option:
|
if option_["id"] == option:
|
||||||
option_type = ARGUMENTS_TYPE_PARSERS[option_["type"]]
|
option_type = OPTIONS[option_["type"]]
|
||||||
break
|
break
|
||||||
|
|
||||||
return option_type.normalize(value) if option_type else value
|
return option_type.normalize(value) if option_type else value
|
||||||
|
@ -152,7 +146,7 @@ class ConfigPanel:
|
||||||
|
|
||||||
if mode == "full":
|
if mode == "full":
|
||||||
option["ask"] = ask
|
option["ask"] = ask
|
||||||
question_class = ARGUMENTS_TYPE_PARSERS[option.get("type", "string")]
|
question_class = OPTIONS[option.get("type", "string")]
|
||||||
# FIXME : maybe other properties should be taken from the question, not just choices ?.
|
# FIXME : maybe other properties should be taken from the question, not just choices ?.
|
||||||
option["choices"] = question_class(option).choices
|
option["choices"] = question_class(option).choices
|
||||||
option["default"] = question_class(option).default
|
option["default"] = question_class(option).default
|
||||||
|
@ -160,9 +154,7 @@ class ConfigPanel:
|
||||||
else:
|
else:
|
||||||
result[key] = {"ask": ask}
|
result[key] = {"ask": ask}
|
||||||
if "current_value" in option:
|
if "current_value" in option:
|
||||||
question_class = ARGUMENTS_TYPE_PARSERS[
|
question_class = OPTIONS[option.get("type", "string")]
|
||||||
option.get("type", "string")
|
|
||||||
]
|
|
||||||
result[key]["value"] = question_class.humanize(
|
result[key]["value"] = question_class.humanize(
|
||||||
option["current_value"], option
|
option["current_value"], option
|
||||||
)
|
)
|
||||||
|
@ -177,6 +169,68 @@ class ConfigPanel:
|
||||||
else:
|
else:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def set(
|
||||||
|
self, key=None, value=None, args=None, args_file=None, operation_logger=None
|
||||||
|
):
|
||||||
|
self.filter_key = key or ""
|
||||||
|
|
||||||
|
# Read config panel toml
|
||||||
|
self._get_config_panel()
|
||||||
|
|
||||||
|
if not self.config:
|
||||||
|
raise YunohostValidationError("config_no_panel")
|
||||||
|
|
||||||
|
if (args is not None or args_file is not None) and value is not None:
|
||||||
|
raise YunohostValidationError(
|
||||||
|
"You should either provide a value, or a serie of args/args_file, but not both at the same time",
|
||||||
|
raw_msg=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.filter_key.count(".") != 2 and value is not None:
|
||||||
|
raise YunohostValidationError("config_cant_set_value_on_section")
|
||||||
|
|
||||||
|
# Import and parse pre-answered options
|
||||||
|
logger.debug("Import and parse pre-answered options")
|
||||||
|
self._parse_pre_answered(args, value, args_file)
|
||||||
|
|
||||||
|
# Read or get values and hydrate the config
|
||||||
|
self._get_raw_settings()
|
||||||
|
self._hydrate()
|
||||||
|
BaseOption.operation_logger = operation_logger
|
||||||
|
self._ask()
|
||||||
|
|
||||||
|
if operation_logger:
|
||||||
|
operation_logger.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._apply()
|
||||||
|
except YunohostError:
|
||||||
|
raise
|
||||||
|
# Script got manually interrupted ...
|
||||||
|
# N.B. : KeyboardInterrupt does not inherit from Exception
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
error = m18n.n("operation_interrupted")
|
||||||
|
logger.error(m18n.n("config_apply_failed", error=error))
|
||||||
|
raise
|
||||||
|
# Something wrong happened in Yunohost's code (most probably hook_exec)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
|
||||||
|
logger.error(m18n.n("config_apply_failed", error=error))
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# Delete files uploaded from API
|
||||||
|
# FIXME : this is currently done in the context of config panels,
|
||||||
|
# but could also happen in the context of app install ... (or anywhere else
|
||||||
|
# where we may parse args etc...)
|
||||||
|
FileOption.clean_upload_dirs()
|
||||||
|
|
||||||
|
self._reload_services()
|
||||||
|
|
||||||
|
logger.success("Config updated as expected")
|
||||||
|
operation_logger.success()
|
||||||
|
|
||||||
def list_actions(self):
|
def list_actions(self):
|
||||||
actions = {}
|
actions = {}
|
||||||
|
|
||||||
|
@ -211,9 +265,9 @@ class ConfigPanel:
|
||||||
self._parse_pre_answered(args, None, args_file)
|
self._parse_pre_answered(args, None, args_file)
|
||||||
|
|
||||||
# Read or get values and hydrate the config
|
# Read or get values and hydrate the config
|
||||||
self._load_current_values()
|
self._get_raw_settings()
|
||||||
self._hydrate()
|
self._hydrate()
|
||||||
Question.operation_logger = operation_logger
|
BaseOption.operation_logger = operation_logger
|
||||||
self._ask(action=action_id)
|
self._ask(action=action_id)
|
||||||
|
|
||||||
# FIXME: here, we could want to check constrains on
|
# FIXME: here, we could want to check constrains on
|
||||||
|
@ -244,75 +298,13 @@ class ConfigPanel:
|
||||||
# FIXME : this is currently done in the context of config panels,
|
# FIXME : this is currently done in the context of config panels,
|
||||||
# but could also happen in the context of app install ... (or anywhere else
|
# but could also happen in the context of app install ... (or anywhere else
|
||||||
# where we may parse args etc...)
|
# where we may parse args etc...)
|
||||||
FileQuestion.clean_upload_dirs()
|
FileOption.clean_upload_dirs()
|
||||||
|
|
||||||
# FIXME: i18n
|
# FIXME: i18n
|
||||||
logger.success(f"Action {action_id} successful")
|
logger.success(f"Action {action_id} successful")
|
||||||
operation_logger.success()
|
operation_logger.success()
|
||||||
|
|
||||||
def set(
|
def _get_raw_config(self):
|
||||||
self, key=None, value=None, args=None, args_file=None, operation_logger=None
|
|
||||||
):
|
|
||||||
self.filter_key = key or ""
|
|
||||||
|
|
||||||
# Read config panel toml
|
|
||||||
self._get_config_panel()
|
|
||||||
|
|
||||||
if not self.config:
|
|
||||||
raise YunohostValidationError("config_no_panel")
|
|
||||||
|
|
||||||
if (args is not None or args_file is not None) and value is not None:
|
|
||||||
raise YunohostValidationError(
|
|
||||||
"You should either provide a value, or a serie of args/args_file, but not both at the same time",
|
|
||||||
raw_msg=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.filter_key.count(".") != 2 and value is not None:
|
|
||||||
raise YunohostValidationError("config_cant_set_value_on_section")
|
|
||||||
|
|
||||||
# Import and parse pre-answered options
|
|
||||||
logger.debug("Import and parse pre-answered options")
|
|
||||||
self._parse_pre_answered(args, value, args_file)
|
|
||||||
|
|
||||||
# Read or get values and hydrate the config
|
|
||||||
self._load_current_values()
|
|
||||||
self._hydrate()
|
|
||||||
Question.operation_logger = operation_logger
|
|
||||||
self._ask()
|
|
||||||
|
|
||||||
if operation_logger:
|
|
||||||
operation_logger.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._apply()
|
|
||||||
except YunohostError:
|
|
||||||
raise
|
|
||||||
# Script got manually interrupted ...
|
|
||||||
# N.B. : KeyboardInterrupt does not inherit from Exception
|
|
||||||
except (KeyboardInterrupt, EOFError):
|
|
||||||
error = m18n.n("operation_interrupted")
|
|
||||||
logger.error(m18n.n("config_apply_failed", error=error))
|
|
||||||
raise
|
|
||||||
# Something wrong happened in Yunohost's code (most probably hook_exec)
|
|
||||||
except Exception:
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
|
|
||||||
logger.error(m18n.n("config_apply_failed", error=error))
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
# Delete files uploaded from API
|
|
||||||
# FIXME : this is currently done in the context of config panels,
|
|
||||||
# but could also happen in the context of app install ... (or anywhere else
|
|
||||||
# where we may parse args etc...)
|
|
||||||
FileQuestion.clean_upload_dirs()
|
|
||||||
|
|
||||||
self._reload_services()
|
|
||||||
|
|
||||||
logger.success("Config updated as expected")
|
|
||||||
operation_logger.success()
|
|
||||||
|
|
||||||
def _get_toml(self):
|
|
||||||
return read_toml(self.config_path)
|
return read_toml(self.config_path)
|
||||||
|
|
||||||
def _get_config_panel(self):
|
def _get_config_panel(self):
|
||||||
|
@ -328,7 +320,7 @@ class ConfigPanel:
|
||||||
logger.debug(f"Config panel {self.config_path} doesn't exists")
|
logger.debug(f"Config panel {self.config_path} doesn't exists")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
toml_config_panel = self._get_toml()
|
toml_config_panel = self._get_raw_config()
|
||||||
|
|
||||||
# Check TOML config panel is in a supported version
|
# Check TOML config panel is in a supported version
|
||||||
if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED:
|
if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED:
|
||||||
|
@ -490,6 +482,26 @@ class ConfigPanel:
|
||||||
|
|
||||||
return self.config
|
return self.config
|
||||||
|
|
||||||
|
def _get_default_values(self):
|
||||||
|
return {
|
||||||
|
option["id"]: option["default"]
|
||||||
|
for _, _, option in self._iterate()
|
||||||
|
if "default" in option
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_raw_settings(self):
|
||||||
|
"""
|
||||||
|
Retrieve entries in YAML file
|
||||||
|
And set default values if needed
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Inject defaults if needed (using the magic .update() ;))
|
||||||
|
self.values = self._get_default_values()
|
||||||
|
|
||||||
|
# Retrieve entries in the YAML
|
||||||
|
if os.path.exists(self.save_path) and os.path.isfile(self.save_path):
|
||||||
|
self.values.update(read_yaml(self.save_path) or {})
|
||||||
|
|
||||||
def _hydrate(self):
|
def _hydrate(self):
|
||||||
# Hydrating config panel with current value
|
# Hydrating config panel with current value
|
||||||
for _, section, option in self._iterate():
|
for _, section, option in self._iterate():
|
||||||
|
@ -606,13 +618,6 @@ class ConfigPanel:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_default_values(self):
|
|
||||||
return {
|
|
||||||
option["id"]: option["default"]
|
|
||||||
for _, _, option in self._iterate()
|
|
||||||
if "default" in option
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def future_values(self):
|
def future_values(self):
|
||||||
return {**self.values, **self.new_values}
|
return {**self.values, **self.new_values}
|
||||||
|
@ -626,19 +631,6 @@ class ConfigPanel:
|
||||||
|
|
||||||
return self.__dict__[name]
|
return self.__dict__[name]
|
||||||
|
|
||||||
def _load_current_values(self):
|
|
||||||
"""
|
|
||||||
Retrieve entries in YAML file
|
|
||||||
And set default values if needed
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Inject defaults if needed (using the magic .update() ;))
|
|
||||||
self.values = self._get_default_values()
|
|
||||||
|
|
||||||
# Retrieve entries in the YAML
|
|
||||||
if os.path.exists(self.save_path) and os.path.isfile(self.save_path):
|
|
||||||
self.values.update(read_yaml(self.save_path) or {})
|
|
||||||
|
|
||||||
def _parse_pre_answered(self, args, value, args_file):
|
def _parse_pre_answered(self, args, value, args_file):
|
||||||
args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
|
args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
|
||||||
self.args = {key: ",".join(value_) for key, value_ in args.items()}
|
self.args = {key: ",".join(value_) for key, value_ in args.items()}
|
||||||
|
|
|
@ -16,30 +16,33 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import urllib.parse
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import ast
|
import ast
|
||||||
import operator as op
|
import operator as op
|
||||||
from typing import Optional, Dict, List, Union, Any, Mapping, Callable
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import urllib.parse
|
||||||
|
from typing import Any, Callable, Dict, List, Mapping, Optional, Union
|
||||||
|
|
||||||
from moulinette.interfaces.cli import colorize
|
|
||||||
from moulinette import Moulinette, m18n
|
from moulinette import Moulinette, m18n
|
||||||
|
from moulinette.interfaces.cli import colorize
|
||||||
|
from moulinette.utils.filesystem import read_file, write_to_file
|
||||||
from moulinette.utils.log import getActionLogger
|
from moulinette.utils.log import getActionLogger
|
||||||
from moulinette.utils.filesystem import (
|
|
||||||
read_file,
|
|
||||||
write_to_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
from yunohost.utils.i18n import _value_for_locale
|
|
||||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
|
||||||
from yunohost.log import OperationLogger
|
from yunohost.log import OperationLogger
|
||||||
|
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||||
|
from yunohost.utils.i18n import _value_for_locale
|
||||||
|
|
||||||
logger = getActionLogger("yunohost.form")
|
logger = getActionLogger("yunohost.form")
|
||||||
|
|
||||||
|
|
||||||
|
# ╭───────────────────────────────────────────────────────╮
|
||||||
|
# │ ┌─╴╷ ╷╭─┐╷ │
|
||||||
|
# │ ├─╴│╭╯├─┤│ │
|
||||||
|
# │ ╰─╴╰╯ ╵ ╵╰─╴ │
|
||||||
|
# ╰───────────────────────────────────────────────────────╯
|
||||||
|
|
||||||
|
|
||||||
# Those js-like evaluate functions are used to eval safely visible attributes
|
# Those js-like evaluate functions are used to eval safely visible attributes
|
||||||
# The goal is to evaluate in the same way than js simple-evaluate
|
# The goal is to evaluate in the same way than js simple-evaluate
|
||||||
# https://github.com/shepherdwind/simple-evaluate
|
# https://github.com/shepherdwind/simple-evaluate
|
||||||
|
@ -183,7 +186,14 @@ def evaluate_simple_js_expression(expr, context={}):
|
||||||
return evaluate_simple_ast(node, context)
|
return evaluate_simple_ast(node, context)
|
||||||
|
|
||||||
|
|
||||||
class Question:
|
# ╭───────────────────────────────────────────────────────╮
|
||||||
|
# │ ╭─╮┌─╮╶┬╴╶┬╴╭─╮╭╮╷╭─╴ │
|
||||||
|
# │ │ │├─╯ │ │ │ ││││╰─╮ │
|
||||||
|
# │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │
|
||||||
|
# ╰───────────────────────────────────────────────────────╯
|
||||||
|
|
||||||
|
|
||||||
|
class BaseOption:
|
||||||
hide_user_input_in_prompt = False
|
hide_user_input_in_prompt = False
|
||||||
pattern: Optional[Dict] = None
|
pattern: Optional[Dict] = None
|
||||||
|
|
||||||
|
@ -231,22 +241,6 @@ class Question:
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def _prompt(self, text):
|
|
||||||
prefill = ""
|
|
||||||
if self.current_value is not None:
|
|
||||||
prefill = self.humanize(self.current_value, self)
|
|
||||||
elif self.default is not None:
|
|
||||||
prefill = self.humanize(self.default, self)
|
|
||||||
self.value = Moulinette.prompt(
|
|
||||||
message=text,
|
|
||||||
is_password=self.hide_user_input_in_prompt,
|
|
||||||
confirm=False,
|
|
||||||
prefill=prefill,
|
|
||||||
is_multiline=(self.type == "text"),
|
|
||||||
autocomplete=self.choices or [],
|
|
||||||
help=_value_for_locale(self.help),
|
|
||||||
)
|
|
||||||
|
|
||||||
def ask_if_needed(self):
|
def ask_if_needed(self):
|
||||||
if self.visible and not evaluate_simple_js_expression(
|
if self.visible and not evaluate_simple_js_expression(
|
||||||
self.visible, context=self.context
|
self.visible, context=self.context
|
||||||
|
@ -279,7 +273,7 @@ class Question:
|
||||||
try:
|
try:
|
||||||
# Normalize and validate
|
# Normalize and validate
|
||||||
self.value = self.normalize(self.value, self)
|
self.value = self.normalize(self.value, self)
|
||||||
self._prevalidate()
|
self._value_pre_validator()
|
||||||
except YunohostValidationError as e:
|
except YunohostValidationError as e:
|
||||||
# If in interactive cli, re-ask the current question
|
# If in interactive cli, re-ask the current question
|
||||||
if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1):
|
if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1):
|
||||||
|
@ -292,7 +286,7 @@ class Question:
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
self.value = self.values[self.name] = self._post_parse_value()
|
self.value = self.values[self.name] = self._value_post_validator()
|
||||||
|
|
||||||
# Search for post actions in hooks
|
# Search for post actions in hooks
|
||||||
post_hook = f"post_ask__{self.name}"
|
post_hook = f"post_ask__{self.name}"
|
||||||
|
@ -301,25 +295,21 @@ class Question:
|
||||||
|
|
||||||
return self.values
|
return self.values
|
||||||
|
|
||||||
def _prevalidate(self):
|
def _prompt(self, text):
|
||||||
if self.value in [None, ""] and not self.optional:
|
prefill = ""
|
||||||
raise YunohostValidationError("app_argument_required", name=self.name)
|
if self.current_value is not None:
|
||||||
|
prefill = self.humanize(self.current_value, self)
|
||||||
# we have an answer, do some post checks
|
elif self.default is not None:
|
||||||
if self.value not in [None, ""]:
|
prefill = self.humanize(self.default, self)
|
||||||
if self.choices and self.value not in self.choices:
|
self.value = Moulinette.prompt(
|
||||||
raise YunohostValidationError(
|
message=text,
|
||||||
"app_argument_choice_invalid",
|
is_password=self.hide_user_input_in_prompt,
|
||||||
name=self.name,
|
confirm=False,
|
||||||
value=self.value,
|
prefill=prefill,
|
||||||
choices=", ".join(str(choice) for choice in self.choices),
|
is_multiline=(self.type == "text"),
|
||||||
)
|
autocomplete=self.choices or [],
|
||||||
if self.pattern and not re.match(self.pattern["regexp"], str(self.value)):
|
help=_value_for_locale(self.help),
|
||||||
raise YunohostValidationError(
|
)
|
||||||
self.pattern["error"],
|
|
||||||
name=self.name,
|
|
||||||
value=self.value,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _format_text_for_user_input_in_cli(self):
|
def _format_text_for_user_input_in_cli(self):
|
||||||
text_for_user_input_in_cli = _value_for_locale(self.ask)
|
text_for_user_input_in_cli = _value_for_locale(self.ask)
|
||||||
|
@ -353,7 +343,27 @@ class Question:
|
||||||
|
|
||||||
return text_for_user_input_in_cli
|
return text_for_user_input_in_cli
|
||||||
|
|
||||||
def _post_parse_value(self):
|
def _value_pre_validator(self):
|
||||||
|
if self.value in [None, ""] and not self.optional:
|
||||||
|
raise YunohostValidationError("app_argument_required", name=self.name)
|
||||||
|
|
||||||
|
# we have an answer, do some post checks
|
||||||
|
if self.value not in [None, ""]:
|
||||||
|
if self.choices and self.value not in self.choices:
|
||||||
|
raise YunohostValidationError(
|
||||||
|
"app_argument_choice_invalid",
|
||||||
|
name=self.name,
|
||||||
|
value=self.value,
|
||||||
|
choices=", ".join(str(choice) for choice in self.choices),
|
||||||
|
)
|
||||||
|
if self.pattern and not re.match(self.pattern["regexp"], str(self.value)):
|
||||||
|
raise YunohostValidationError(
|
||||||
|
self.pattern["error"],
|
||||||
|
name=self.name,
|
||||||
|
value=self.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _value_post_validator(self):
|
||||||
if not self.redact:
|
if not self.redact:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
@ -377,108 +387,66 @@ class Question:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class StringQuestion(Question):
|
# ╭───────────────────────────────────────────────────────╮
|
||||||
|
# │ DISPLAY OPTIONS │
|
||||||
|
# ╰───────────────────────────────────────────────────────╯
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayTextOption(BaseOption):
|
||||||
|
argument_type = "display_text"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
||||||
|
):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
|
|
||||||
|
self.optional = True
|
||||||
|
self.readonly = True
|
||||||
|
self.style = question.get(
|
||||||
|
"style", "info" if question["type"] == "alert" else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
def _format_text_for_user_input_in_cli(self):
|
||||||
|
text = _value_for_locale(self.ask)
|
||||||
|
|
||||||
|
if self.style in ["success", "info", "warning", "danger"]:
|
||||||
|
color = {
|
||||||
|
"success": "green",
|
||||||
|
"info": "cyan",
|
||||||
|
"warning": "yellow",
|
||||||
|
"danger": "red",
|
||||||
|
}
|
||||||
|
prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger")
|
||||||
|
return colorize(prompt, color[self.style]) + f" {text}"
|
||||||
|
else:
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonOption(BaseOption):
|
||||||
|
argument_type = "button"
|
||||||
|
enabled = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
||||||
|
):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
|
self.enabled = question.get("enabled", None)
|
||||||
|
|
||||||
|
|
||||||
|
# ╭───────────────────────────────────────────────────────╮
|
||||||
|
# │ INPUT OPTIONS │
|
||||||
|
# ╰───────────────────────────────────────────────────────╯
|
||||||
|
|
||||||
|
|
||||||
|
# ─ STRINGS ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class StringOption(BaseOption):
|
||||||
argument_type = "string"
|
argument_type = "string"
|
||||||
default_value = ""
|
default_value = ""
|
||||||
|
|
||||||
|
|
||||||
class EmailQuestion(StringQuestion):
|
class PasswordOption(BaseOption):
|
||||||
pattern = {
|
|
||||||
"regexp": r"^.+@.+",
|
|
||||||
"error": "config_validate_email", # i18n: config_validate_email
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class URLQuestion(StringQuestion):
|
|
||||||
pattern = {
|
|
||||||
"regexp": r"^https?://.*$",
|
|
||||||
"error": "config_validate_url", # i18n: config_validate_url
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DateQuestion(StringQuestion):
|
|
||||||
pattern = {
|
|
||||||
"regexp": r"^\d{4}-\d\d-\d\d$",
|
|
||||||
"error": "config_validate_date", # i18n: config_validate_date
|
|
||||||
}
|
|
||||||
|
|
||||||
def _prevalidate(self):
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
super()._prevalidate()
|
|
||||||
|
|
||||||
if self.value not in [None, ""]:
|
|
||||||
try:
|
|
||||||
datetime.strptime(self.value, "%Y-%m-%d")
|
|
||||||
except ValueError:
|
|
||||||
raise YunohostValidationError("config_validate_date")
|
|
||||||
|
|
||||||
|
|
||||||
class TimeQuestion(StringQuestion):
|
|
||||||
pattern = {
|
|
||||||
"regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$",
|
|
||||||
"error": "config_validate_time", # i18n: config_validate_time
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ColorQuestion(StringQuestion):
|
|
||||||
pattern = {
|
|
||||||
"regexp": r"^#[ABCDEFabcdef\d]{3,6}$",
|
|
||||||
"error": "config_validate_color", # i18n: config_validate_color
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TagsQuestion(Question):
|
|
||||||
argument_type = "tags"
|
|
||||||
default_value = ""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def humanize(value, option={}):
|
|
||||||
if isinstance(value, list):
|
|
||||||
return ",".join(str(v) for v in value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def normalize(value, option={}):
|
|
||||||
if isinstance(value, list):
|
|
||||||
return ",".join(str(v) for v in value)
|
|
||||||
if isinstance(value, str):
|
|
||||||
value = value.strip()
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _prevalidate(self):
|
|
||||||
values = self.value
|
|
||||||
if isinstance(values, str):
|
|
||||||
values = values.split(",")
|
|
||||||
elif values is None:
|
|
||||||
values = []
|
|
||||||
|
|
||||||
if not isinstance(values, list):
|
|
||||||
if self.choices:
|
|
||||||
raise YunohostValidationError(
|
|
||||||
"app_argument_choice_invalid",
|
|
||||||
name=self.name,
|
|
||||||
value=self.value,
|
|
||||||
choices=", ".join(str(choice) for choice in self.choices),
|
|
||||||
)
|
|
||||||
raise YunohostValidationError(
|
|
||||||
"app_argument_invalid",
|
|
||||||
name=self.name,
|
|
||||||
error=f"'{str(self.value)}' is not a list",
|
|
||||||
)
|
|
||||||
|
|
||||||
for value in values:
|
|
||||||
self.value = value
|
|
||||||
super()._prevalidate()
|
|
||||||
self.value = values
|
|
||||||
|
|
||||||
def _post_parse_value(self):
|
|
||||||
if isinstance(self.value, list):
|
|
||||||
self.value = ",".join(self.value)
|
|
||||||
return super()._post_parse_value()
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordQuestion(Question):
|
|
||||||
hide_user_input_in_prompt = True
|
hide_user_input_in_prompt = True
|
||||||
argument_type = "password"
|
argument_type = "password"
|
||||||
default_value = ""
|
default_value = ""
|
||||||
|
@ -494,8 +462,8 @@ class PasswordQuestion(Question):
|
||||||
"app_argument_password_no_default", name=self.name
|
"app_argument_password_no_default", name=self.name
|
||||||
)
|
)
|
||||||
|
|
||||||
def _prevalidate(self):
|
def _value_pre_validator(self):
|
||||||
super()._prevalidate()
|
super()._value_pre_validator()
|
||||||
|
|
||||||
if self.value not in [None, ""]:
|
if self.value not in [None, ""]:
|
||||||
if any(char in self.value for char in self.forbidden_chars):
|
if any(char in self.value for char in self.forbidden_chars):
|
||||||
|
@ -509,51 +477,95 @@ class PasswordQuestion(Question):
|
||||||
assert_password_is_strong_enough("user", self.value)
|
assert_password_is_strong_enough("user", self.value)
|
||||||
|
|
||||||
|
|
||||||
class PathQuestion(Question):
|
class ColorOption(StringOption):
|
||||||
argument_type = "path"
|
pattern = {
|
||||||
default_value = ""
|
"regexp": r"^#[ABCDEFabcdef\d]{3,6}$",
|
||||||
|
"error": "config_validate_color", # i18n: config_validate_color
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─ NUMERIC ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class NumberOption(BaseOption):
|
||||||
|
argument_type = "number"
|
||||||
|
default_value = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
||||||
|
):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
|
self.min = question.get("min", None)
|
||||||
|
self.max = question.get("max", None)
|
||||||
|
self.step = question.get("step", None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize(value, option={}):
|
def normalize(value, option={}):
|
||||||
option = option.__dict__ if isinstance(option, Question) else option
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
|
||||||
if not isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
|
||||||
|
if isinstance(value, str) and value.isdigit():
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
if value in [None, ""]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
option = option.__dict__ if isinstance(option, BaseOption) else option
|
||||||
|
raise YunohostValidationError(
|
||||||
|
"app_argument_invalid",
|
||||||
|
name=option.get("name"),
|
||||||
|
error=m18n.n("invalid_number"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _value_pre_validator(self):
|
||||||
|
super()._value_pre_validator()
|
||||||
|
if self.value in [None, ""]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.min is not None and int(self.value) < self.min:
|
||||||
raise YunohostValidationError(
|
raise YunohostValidationError(
|
||||||
"app_argument_invalid",
|
"app_argument_invalid",
|
||||||
name=option.get("name"),
|
name=self.name,
|
||||||
error="Argument for path should be a string.",
|
error=m18n.n("invalid_number_min", min=self.min),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not value.strip():
|
if self.max is not None and int(self.value) > self.max:
|
||||||
if option.get("optional"):
|
raise YunohostValidationError(
|
||||||
return ""
|
"app_argument_invalid",
|
||||||
# Hmpf here we could just have a "else" case
|
name=self.name,
|
||||||
# but we also want PathQuestion.normalize("") to return "/"
|
error=m18n.n("invalid_number_max", max=self.max),
|
||||||
# (i.e. if no option is provided, hence .get("optional") is None
|
)
|
||||||
elif option.get("optional") is False:
|
|
||||||
raise YunohostValidationError(
|
|
||||||
"app_argument_invalid",
|
|
||||||
name=option.get("name"),
|
|
||||||
error="Question is mandatory",
|
|
||||||
)
|
|
||||||
|
|
||||||
return "/" + value.strip().strip(" /")
|
|
||||||
|
|
||||||
|
|
||||||
class BooleanQuestion(Question):
|
# ─ BOOLEAN ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanOption(BaseOption):
|
||||||
argument_type = "boolean"
|
argument_type = "boolean"
|
||||||
default_value = 0
|
default_value = 0
|
||||||
yes_answers = ["1", "yes", "y", "true", "t", "on"]
|
yes_answers = ["1", "yes", "y", "true", "t", "on"]
|
||||||
no_answers = ["0", "no", "n", "false", "f", "off"]
|
no_answers = ["0", "no", "n", "false", "f", "off"]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
||||||
|
):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
|
self.yes = question.get("yes", 1)
|
||||||
|
self.no = question.get("no", 0)
|
||||||
|
if self.default is None:
|
||||||
|
self.default = self.no
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def humanize(value, option={}):
|
def humanize(value, option={}):
|
||||||
option = option.__dict__ if isinstance(option, Question) else option
|
option = option.__dict__ if isinstance(option, BaseOption) else option
|
||||||
|
|
||||||
yes = option.get("yes", 1)
|
yes = option.get("yes", 1)
|
||||||
no = option.get("no", 0)
|
no = option.get("no", 0)
|
||||||
|
|
||||||
value = BooleanQuestion.normalize(value, option)
|
value = BooleanOption.normalize(value, option)
|
||||||
|
|
||||||
if value == yes:
|
if value == yes:
|
||||||
return "yes"
|
return "yes"
|
||||||
|
@ -571,7 +583,7 @@ class BooleanQuestion(Question):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize(value, option={}):
|
def normalize(value, option={}):
|
||||||
option = option.__dict__ if isinstance(option, Question) else option
|
option = option.__dict__ if isinstance(option, BaseOption) else option
|
||||||
|
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
|
@ -579,8 +591,8 @@ class BooleanQuestion(Question):
|
||||||
technical_yes = option.get("yes", 1)
|
technical_yes = option.get("yes", 1)
|
||||||
technical_no = option.get("no", 0)
|
technical_no = option.get("no", 0)
|
||||||
|
|
||||||
no_answers = BooleanQuestion.no_answers
|
no_answers = BooleanOption.no_answers
|
||||||
yes_answers = BooleanQuestion.yes_answers
|
yes_answers = BooleanOption.yes_answers
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
str(technical_yes).lower() not in no_answers
|
str(technical_yes).lower() not in no_answers
|
||||||
|
@ -609,14 +621,8 @@ class BooleanQuestion(Question):
|
||||||
choices="yes/no",
|
choices="yes/no",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def get(self, key, default=None):
|
||||||
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
return getattr(self, key, default)
|
||||||
):
|
|
||||||
super().__init__(question, context, hooks)
|
|
||||||
self.yes = question.get("yes", 1)
|
|
||||||
self.no = question.get("no", 0)
|
|
||||||
if self.default is None:
|
|
||||||
self.default = self.no
|
|
||||||
|
|
||||||
def _format_text_for_user_input_in_cli(self):
|
def _format_text_for_user_input_in_cli(self):
|
||||||
text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli()
|
text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli()
|
||||||
|
@ -626,11 +632,205 @@ class BooleanQuestion(Question):
|
||||||
|
|
||||||
return text_for_user_input_in_cli
|
return text_for_user_input_in_cli
|
||||||
|
|
||||||
def get(self, key, default=None):
|
|
||||||
return getattr(self, key, default)
|
# ─ TIME ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
class DomainQuestion(Question):
|
class DateOption(StringOption):
|
||||||
|
pattern = {
|
||||||
|
"regexp": r"^\d{4}-\d\d-\d\d$",
|
||||||
|
"error": "config_validate_date", # i18n: config_validate_date
|
||||||
|
}
|
||||||
|
|
||||||
|
def _value_pre_validator(self):
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
super()._value_pre_validator()
|
||||||
|
|
||||||
|
if self.value not in [None, ""]:
|
||||||
|
try:
|
||||||
|
datetime.strptime(self.value, "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
raise YunohostValidationError("config_validate_date")
|
||||||
|
|
||||||
|
|
||||||
|
class TimeOption(StringOption):
|
||||||
|
pattern = {
|
||||||
|
"regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$",
|
||||||
|
"error": "config_validate_time", # i18n: config_validate_time
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─ LOCATIONS ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class EmailOption(StringOption):
|
||||||
|
pattern = {
|
||||||
|
"regexp": r"^.+@.+",
|
||||||
|
"error": "config_validate_email", # i18n: config_validate_email
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WebPathOption(BaseOption):
|
||||||
|
argument_type = "path"
|
||||||
|
default_value = ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalize(value, option={}):
|
||||||
|
option = option.__dict__ if isinstance(option, BaseOption) else option
|
||||||
|
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise YunohostValidationError(
|
||||||
|
"app_argument_invalid",
|
||||||
|
name=option.get("name"),
|
||||||
|
error="Argument for path should be a string.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not value.strip():
|
||||||
|
if option.get("optional"):
|
||||||
|
return ""
|
||||||
|
# Hmpf here we could just have a "else" case
|
||||||
|
# but we also want WebPathOption.normalize("") to return "/"
|
||||||
|
# (i.e. if no option is provided, hence .get("optional") is None
|
||||||
|
elif option.get("optional") is False:
|
||||||
|
raise YunohostValidationError(
|
||||||
|
"app_argument_invalid",
|
||||||
|
name=option.get("name"),
|
||||||
|
error="Option is mandatory",
|
||||||
|
)
|
||||||
|
|
||||||
|
return "/" + value.strip().strip(" /")
|
||||||
|
|
||||||
|
|
||||||
|
class URLOption(StringOption):
|
||||||
|
pattern = {
|
||||||
|
"regexp": r"^https?://.*$",
|
||||||
|
"error": "config_validate_url", # i18n: config_validate_url
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─ FILE ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class FileOption(BaseOption):
|
||||||
|
argument_type = "file"
|
||||||
|
upload_dirs: List[str] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
||||||
|
):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
|
self.accept = question.get("accept", "")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clean_upload_dirs(cls):
|
||||||
|
# Delete files uploaded from API
|
||||||
|
for upload_dir in cls.upload_dirs:
|
||||||
|
if os.path.exists(upload_dir):
|
||||||
|
shutil.rmtree(upload_dir)
|
||||||
|
|
||||||
|
def _value_pre_validator(self):
|
||||||
|
if self.value is None:
|
||||||
|
self.value = self.current_value
|
||||||
|
|
||||||
|
super()._value_pre_validator()
|
||||||
|
|
||||||
|
# Validation should have already failed if required
|
||||||
|
if self.value in [None, ""]:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
if Moulinette.interface.type != "api":
|
||||||
|
if not os.path.exists(str(self.value)) or not os.path.isfile(
|
||||||
|
str(self.value)
|
||||||
|
):
|
||||||
|
raise YunohostValidationError(
|
||||||
|
"app_argument_invalid",
|
||||||
|
name=self.name,
|
||||||
|
error=m18n.n("file_does_not_exist", path=str(self.value)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _value_post_validator(self):
|
||||||
|
from base64 import b64decode
|
||||||
|
|
||||||
|
if not self.value:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_")
|
||||||
|
_, file_path = tempfile.mkstemp(dir=upload_dir)
|
||||||
|
|
||||||
|
FileOption.upload_dirs += [upload_dir]
|
||||||
|
|
||||||
|
logger.debug(f"Saving file {self.name} for file question into {file_path}")
|
||||||
|
|
||||||
|
def is_file_path(s):
|
||||||
|
return isinstance(s, str) and s.startswith("/") and os.path.exists(s)
|
||||||
|
|
||||||
|
if Moulinette.interface.type != "api" or is_file_path(self.value):
|
||||||
|
content = read_file(str(self.value), file_mode="rb")
|
||||||
|
else:
|
||||||
|
content = b64decode(self.value)
|
||||||
|
|
||||||
|
write_to_file(file_path, content, file_mode="wb")
|
||||||
|
|
||||||
|
self.value = file_path
|
||||||
|
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
# ─ CHOICES ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TagsOption(BaseOption):
|
||||||
|
argument_type = "tags"
|
||||||
|
default_value = ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def humanize(value, option={}):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return ",".join(str(v) for v in value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalize(value, option={}):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return ",".join(str(v) for v in value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _value_pre_validator(self):
|
||||||
|
values = self.value
|
||||||
|
if isinstance(values, str):
|
||||||
|
values = values.split(",")
|
||||||
|
elif values is None:
|
||||||
|
values = []
|
||||||
|
|
||||||
|
if not isinstance(values, list):
|
||||||
|
if self.choices:
|
||||||
|
raise YunohostValidationError(
|
||||||
|
"app_argument_choice_invalid",
|
||||||
|
name=self.name,
|
||||||
|
value=self.value,
|
||||||
|
choices=", ".join(str(choice) for choice in self.choices),
|
||||||
|
)
|
||||||
|
raise YunohostValidationError(
|
||||||
|
"app_argument_invalid",
|
||||||
|
name=self.name,
|
||||||
|
error=f"'{str(self.value)}' is not a list",
|
||||||
|
)
|
||||||
|
|
||||||
|
for value in values:
|
||||||
|
self.value = value
|
||||||
|
super()._value_pre_validator()
|
||||||
|
self.value = values
|
||||||
|
|
||||||
|
def _value_post_validator(self):
|
||||||
|
if isinstance(self.value, list):
|
||||||
|
self.value = ",".join(self.value)
|
||||||
|
return super()._value_post_validator()
|
||||||
|
|
||||||
|
|
||||||
|
class DomainOption(BaseOption):
|
||||||
argument_type = "domain"
|
argument_type = "domain"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -661,7 +861,7 @@ class DomainQuestion(Question):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class AppQuestion(Question):
|
class AppOption(BaseOption):
|
||||||
argument_type = "app"
|
argument_type = "app"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -688,7 +888,7 @@ class AppQuestion(Question):
|
||||||
self.choices.update({app["id"]: _app_display(app) for app in apps})
|
self.choices.update({app["id"]: _app_display(app) for app in apps})
|
||||||
|
|
||||||
|
|
||||||
class UserQuestion(Question):
|
class UserOption(BaseOption):
|
||||||
argument_type = "user"
|
argument_type = "user"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -721,7 +921,7 @@ class UserQuestion(Question):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
class GroupQuestion(Question):
|
class GroupOption(BaseOption):
|
||||||
argument_type = "group"
|
argument_type = "group"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -747,198 +947,46 @@ class GroupQuestion(Question):
|
||||||
self.default = "all_users"
|
self.default = "all_users"
|
||||||
|
|
||||||
|
|
||||||
class NumberQuestion(Question):
|
OPTIONS = {
|
||||||
argument_type = "number"
|
"display_text": DisplayTextOption,
|
||||||
default_value = None
|
"markdown": DisplayTextOption,
|
||||||
|
"alert": DisplayTextOption,
|
||||||
def __init__(
|
"button": ButtonOption,
|
||||||
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
"string": StringOption,
|
||||||
):
|
"text": StringOption,
|
||||||
super().__init__(question, context, hooks)
|
"password": PasswordOption,
|
||||||
self.min = question.get("min", None)
|
"color": ColorOption,
|
||||||
self.max = question.get("max", None)
|
"number": NumberOption,
|
||||||
self.step = question.get("step", None)
|
"range": NumberOption,
|
||||||
|
"boolean": BooleanOption,
|
||||||
@staticmethod
|
"date": DateOption,
|
||||||
def normalize(value, option={}):
|
"time": TimeOption,
|
||||||
if isinstance(value, int):
|
"email": EmailOption,
|
||||||
return value
|
"path": WebPathOption,
|
||||||
|
"url": URLOption,
|
||||||
if isinstance(value, str):
|
"file": FileOption,
|
||||||
value = value.strip()
|
"select": StringOption,
|
||||||
|
"tags": TagsOption,
|
||||||
if isinstance(value, str) and value.isdigit():
|
"domain": DomainOption,
|
||||||
return int(value)
|
"app": AppOption,
|
||||||
|
"user": UserOption,
|
||||||
if value in [None, ""]:
|
"group": GroupOption,
|
||||||
return None
|
|
||||||
|
|
||||||
option = option.__dict__ if isinstance(option, Question) else option
|
|
||||||
raise YunohostValidationError(
|
|
||||||
"app_argument_invalid",
|
|
||||||
name=option.get("name"),
|
|
||||||
error=m18n.n("invalid_number"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _prevalidate(self):
|
|
||||||
super()._prevalidate()
|
|
||||||
if self.value in [None, ""]:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.min is not None and int(self.value) < self.min:
|
|
||||||
raise YunohostValidationError(
|
|
||||||
"app_argument_invalid",
|
|
||||||
name=self.name,
|
|
||||||
error=m18n.n("invalid_number_min", min=self.min),
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.max is not None and int(self.value) > self.max:
|
|
||||||
raise YunohostValidationError(
|
|
||||||
"app_argument_invalid",
|
|
||||||
name=self.name,
|
|
||||||
error=m18n.n("invalid_number_max", max=self.max),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DisplayTextQuestion(Question):
|
|
||||||
argument_type = "display_text"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
|
||||||
):
|
|
||||||
super().__init__(question, context, hooks)
|
|
||||||
|
|
||||||
self.optional = True
|
|
||||||
self.readonly = True
|
|
||||||
self.style = question.get(
|
|
||||||
"style", "info" if question["type"] == "alert" else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
def _format_text_for_user_input_in_cli(self):
|
|
||||||
text = _value_for_locale(self.ask)
|
|
||||||
|
|
||||||
if self.style in ["success", "info", "warning", "danger"]:
|
|
||||||
color = {
|
|
||||||
"success": "green",
|
|
||||||
"info": "cyan",
|
|
||||||
"warning": "yellow",
|
|
||||||
"danger": "red",
|
|
||||||
}
|
|
||||||
prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger")
|
|
||||||
return colorize(prompt, color[self.style]) + f" {text}"
|
|
||||||
else:
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
class FileQuestion(Question):
|
|
||||||
argument_type = "file"
|
|
||||||
upload_dirs: List[str] = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def clean_upload_dirs(cls):
|
|
||||||
# Delete files uploaded from API
|
|
||||||
for upload_dir in cls.upload_dirs:
|
|
||||||
if os.path.exists(upload_dir):
|
|
||||||
shutil.rmtree(upload_dir)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
|
||||||
):
|
|
||||||
super().__init__(question, context, hooks)
|
|
||||||
self.accept = question.get("accept", "")
|
|
||||||
|
|
||||||
def _prevalidate(self):
|
|
||||||
if self.value is None:
|
|
||||||
self.value = self.current_value
|
|
||||||
|
|
||||||
super()._prevalidate()
|
|
||||||
|
|
||||||
# Validation should have already failed if required
|
|
||||||
if self.value in [None, ""]:
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
if Moulinette.interface.type != "api":
|
|
||||||
if not os.path.exists(str(self.value)) or not os.path.isfile(
|
|
||||||
str(self.value)
|
|
||||||
):
|
|
||||||
raise YunohostValidationError(
|
|
||||||
"app_argument_invalid",
|
|
||||||
name=self.name,
|
|
||||||
error=m18n.n("file_does_not_exist", path=str(self.value)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _post_parse_value(self):
|
|
||||||
from base64 import b64decode
|
|
||||||
|
|
||||||
if not self.value:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_")
|
|
||||||
_, file_path = tempfile.mkstemp(dir=upload_dir)
|
|
||||||
|
|
||||||
FileQuestion.upload_dirs += [upload_dir]
|
|
||||||
|
|
||||||
logger.debug(f"Saving file {self.name} for file question into {file_path}")
|
|
||||||
|
|
||||||
def is_file_path(s):
|
|
||||||
return isinstance(s, str) and s.startswith("/") and os.path.exists(s)
|
|
||||||
|
|
||||||
if Moulinette.interface.type != "api" or is_file_path(self.value):
|
|
||||||
content = read_file(str(self.value), file_mode="rb")
|
|
||||||
else:
|
|
||||||
content = b64decode(self.value)
|
|
||||||
|
|
||||||
write_to_file(file_path, content, file_mode="wb")
|
|
||||||
|
|
||||||
self.value = file_path
|
|
||||||
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
class ButtonQuestion(Question):
|
|
||||||
argument_type = "button"
|
|
||||||
enabled = None
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}
|
|
||||||
):
|
|
||||||
super().__init__(question, context, hooks)
|
|
||||||
self.enabled = question.get("enabled", None)
|
|
||||||
|
|
||||||
|
|
||||||
ARGUMENTS_TYPE_PARSERS = {
|
|
||||||
"string": StringQuestion,
|
|
||||||
"text": StringQuestion,
|
|
||||||
"select": StringQuestion,
|
|
||||||
"tags": TagsQuestion,
|
|
||||||
"email": EmailQuestion,
|
|
||||||
"url": URLQuestion,
|
|
||||||
"date": DateQuestion,
|
|
||||||
"time": TimeQuestion,
|
|
||||||
"color": ColorQuestion,
|
|
||||||
"password": PasswordQuestion,
|
|
||||||
"path": PathQuestion,
|
|
||||||
"boolean": BooleanQuestion,
|
|
||||||
"domain": DomainQuestion,
|
|
||||||
"user": UserQuestion,
|
|
||||||
"group": GroupQuestion,
|
|
||||||
"number": NumberQuestion,
|
|
||||||
"range": NumberQuestion,
|
|
||||||
"display_text": DisplayTextQuestion,
|
|
||||||
"alert": DisplayTextQuestion,
|
|
||||||
"markdown": DisplayTextQuestion,
|
|
||||||
"file": FileQuestion,
|
|
||||||
"app": AppQuestion,
|
|
||||||
"button": ButtonQuestion,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ╭───────────────────────────────────────────────────────╮
|
||||||
|
# │ ╷ ╷╶┬╴╶┬╴╷ ╭─╴ │
|
||||||
|
# │ │ │ │ │ │ ╰─╮ │
|
||||||
|
# │ ╰─╯ ╵ ╶┴╴╰─╴╶─╯ │
|
||||||
|
# ╰───────────────────────────────────────────────────────╯
|
||||||
|
|
||||||
|
|
||||||
def ask_questions_and_parse_answers(
|
def ask_questions_and_parse_answers(
|
||||||
raw_questions: Dict,
|
raw_questions: Dict,
|
||||||
prefilled_answers: Union[str, Mapping[str, Any]] = {},
|
prefilled_answers: Union[str, Mapping[str, Any]] = {},
|
||||||
current_values: Mapping[str, Any] = {},
|
current_values: Mapping[str, Any] = {},
|
||||||
hooks: Dict[str, Callable[[], None]] = {},
|
hooks: Dict[str, Callable[[], None]] = {},
|
||||||
) -> List[Question]:
|
) -> List[BaseOption]:
|
||||||
"""Parse arguments store in either manifest.json or actions.json or from a
|
"""Parse arguments store in either manifest.json or actions.json or from a
|
||||||
config panel against the user answers when they are present.
|
config panel against the user answers when they are present.
|
||||||
|
|
||||||
|
@ -969,7 +1017,7 @@ def ask_questions_and_parse_answers(
|
||||||
|
|
||||||
for name, raw_question in raw_questions.items():
|
for name, raw_question in raw_questions.items():
|
||||||
raw_question["name"] = name
|
raw_question["name"] = name
|
||||||
question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]
|
question_class = OPTIONS[raw_question.get("type", "string")]
|
||||||
raw_question["value"] = answers.get(name)
|
raw_question["value"] = answers.get(name)
|
||||||
question = question_class(raw_question, context=context, hooks=hooks)
|
question = question_class(raw_question, context=context, hooks=hooks)
|
||||||
if question.type == "button":
|
if question.type == "button":
|
||||||
|
@ -996,9 +1044,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List:
|
||||||
out = []
|
out = []
|
||||||
|
|
||||||
for raw_question in raw_questions:
|
for raw_question in raw_questions:
|
||||||
question = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")](
|
question = OPTIONS[raw_question.get("type", "string")](raw_question)
|
||||||
raw_question
|
|
||||||
)
|
|
||||||
if question.choices:
|
if question.choices:
|
||||||
raw_question["choices"] = question.choices
|
raw_question["choices"] = question.choices
|
||||||
raw_question["default"] = question.default
|
raw_question["default"] = question.default
|
||||||
|
|
Loading…
Add table
Reference in a new issue