From f9fd3799979600581542ad01bf85a937622f98f2 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 7 Apr 2023 16:25:48 +0200 Subject: [PATCH 1/9] form: rename Questions to Options --- src/app.py | 20 +++--- src/domain.py | 4 +- src/settings.py | 6 +- src/tests/test_questions.py | 138 ++++++++++++++++++------------------ src/utils/configpanel.py | 12 ++-- src/utils/form.py | 106 +++++++++++++-------------- 6 files changed, 143 insertions(+), 143 deletions(-) diff --git a/src/app.py b/src/app.py index 91b55b39d..96225e7b2 100644 --- a/src/app.py +++ b/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.form import ( - DomainQuestion, - PathQuestion, + DomainOption, + WebPathOption, hydrate_questions_with_choices, ) 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 - domain = DomainQuestion.normalize(domain) - old_domain = DomainQuestion.normalize(old_domain) - path = PathQuestion.normalize(path) - old_path = PathQuestion.normalize(old_path) + domain = DomainOption.normalize(domain) + old_domain = DomainOption.normalize(old_domain) + path = WebPathOption.normalize(path) + old_path = WebPathOption.normalize(old_path) if (domain, path) == (old_domain, old_path): raise YunohostValidationError( @@ -1660,8 +1660,8 @@ def app_register_url(app, domain, path): permission_sync_to_user, ) - domain = DomainQuestion.normalize(domain) - path = PathQuestion.normalize(path) + domain = DomainOption.normalize(domain) + path = WebPathOption.normalize(path) # We cannot change the url of an app already installed simply by changing # the settings... @@ -2853,8 +2853,8 @@ def _get_conflicting_apps(domain, path, ignore_app=None): from yunohost.domain import _assert_domain_exists - domain = DomainQuestion.normalize(domain) - path = PathQuestion.normalize(path) + domain = DomainOption.normalize(domain) + path = WebPathOption.normalize(path) # Abort if domain is unknown _assert_domain_exists(domain) diff --git a/src/domain.py b/src/domain.py index 9f38d6765..498c2417a 100644 --- a/src/domain.py +++ b/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.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.log import is_unit_operation @@ -528,7 +528,7 @@ def domain_config_set( """ Apply a new domain configuration """ - Question.operation_logger = operation_logger + BaseOption.operation_logger = operation_logger config = DomainConfigPanel(domain) return config.set(key, value, args, args_file, operation_logger=operation_logger) diff --git a/src/settings.py b/src/settings.py index 5d52329b3..f863ef74d 100644 --- a/src/settings.py +++ b/src/settings.py @@ -22,7 +22,7 @@ import subprocess from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError 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 yunohost.regenconf import regen_conf 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 """ - Question.operation_logger = operation_logger + BaseOption.operation_logger = operation_logger settings = SettingsConfigPanel() key = translate_legacy_settings_to_configpanel_settings(key) return settings.set(key, value, args, args_file, operation_logger=operation_logger) @@ -231,7 +231,7 @@ class SettingsConfigPanel(ConfigPanel): # Replace all values with default values self.values = self._get_default_values() - Question.operation_logger = operation_logger + BaseOption.operation_logger = operation_logger if operation_logger: operation_logger.start() diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 506fde077..7579355bd 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -17,12 +17,12 @@ from yunohost import app, domain, user from yunohost.utils.form import ( ARGUMENTS_TYPE_PARSERS, ask_questions_and_parse_answers, - DisplayTextQuestion, - PasswordQuestion, - DomainQuestion, - PathQuestion, - BooleanQuestion, - FileQuestion, + DisplayTextOption, + PasswordOption, + DomainOption, + WebPathOption, + BooleanOption, + FileOption, evaluate_simple_js_expression, ) from yunohost.utils.error import YunohostError, YunohostValidationError @@ -438,7 +438,7 @@ class BaseTest: id_ = raw_option["id"] 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 option.type == raw_option["type"] @@ -734,7 +734,7 @@ class TestPassword(BaseTest): ], reason="Should output exactly the same"), ("s3cr3t!!", "s3cr3t!!"), ("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 *xpass(scenarios=[ ("s3cr3t!!", "s3cr3t!!", {"readonly": True}), @@ -1225,9 +1225,9 @@ class TestUrl(BaseTest): @pytest.fixture def file_clean(): - FileQuestion.clean_upload_dirs() + FileOption.clean_upload_dirs() yield - FileQuestion.clean_upload_dirs() + FileOption.clean_upload_dirs() @contextmanager @@ -1263,7 +1263,7 @@ def _test_file_intake_may_fail(raw_option, intake, expected_output): with open(value) as f: assert f.read() == expected_output - FileQuestion.clean_upload_dirs() + FileOption.clean_upload_dirs() assert not os.path.exists(value) @@ -2138,88 +2138,88 @@ def test_question_number_input_test_ask_with_example(): def test_normalize_boolean_nominal(): - assert BooleanQuestion.normalize("yes") == 1 - assert BooleanQuestion.normalize("Yes") == 1 - assert BooleanQuestion.normalize(" yes ") == 1 - assert BooleanQuestion.normalize("y") == 1 - assert BooleanQuestion.normalize("true") == 1 - assert BooleanQuestion.normalize("True") == 1 - assert BooleanQuestion.normalize("on") == 1 - assert BooleanQuestion.normalize("1") == 1 - assert BooleanQuestion.normalize(1) == 1 + assert BooleanOption.normalize("yes") == 1 + assert BooleanOption.normalize("Yes") == 1 + assert BooleanOption.normalize(" yes ") == 1 + assert BooleanOption.normalize("y") == 1 + assert BooleanOption.normalize("true") == 1 + assert BooleanOption.normalize("True") == 1 + assert BooleanOption.normalize("on") == 1 + assert BooleanOption.normalize("1") == 1 + assert BooleanOption.normalize(1) == 1 - assert BooleanQuestion.normalize("no") == 0 - assert BooleanQuestion.normalize("No") == 0 - assert BooleanQuestion.normalize(" no ") == 0 - assert BooleanQuestion.normalize("n") == 0 - assert BooleanQuestion.normalize("false") == 0 - assert BooleanQuestion.normalize("False") == 0 - assert BooleanQuestion.normalize("off") == 0 - assert BooleanQuestion.normalize("0") == 0 - assert BooleanQuestion.normalize(0) == 0 + assert BooleanOption.normalize("no") == 0 + assert BooleanOption.normalize("No") == 0 + assert BooleanOption.normalize(" no ") == 0 + assert BooleanOption.normalize("n") == 0 + assert BooleanOption.normalize("false") == 0 + assert BooleanOption.normalize("False") == 0 + assert BooleanOption.normalize("off") == 0 + assert BooleanOption.normalize("0") == 0 + assert BooleanOption.normalize(0) == 0 - assert BooleanQuestion.normalize("") is None - assert BooleanQuestion.normalize(" ") is None - assert BooleanQuestion.normalize(" none ") is None - assert BooleanQuestion.normalize("None") is None - assert BooleanQuestion.normalize("noNe") is None - assert BooleanQuestion.normalize(None) is None + assert BooleanOption.normalize("") is None + assert BooleanOption.normalize(" ") is None + assert BooleanOption.normalize(" none ") is None + assert BooleanOption.normalize("None") is None + assert BooleanOption.normalize("noNe") is None + assert BooleanOption.normalize(None) is None def test_normalize_boolean_humanize(): - assert BooleanQuestion.humanize("yes") == "yes" - assert BooleanQuestion.humanize("true") == "yes" - assert BooleanQuestion.humanize("on") == "yes" + assert BooleanOption.humanize("yes") == "yes" + assert BooleanOption.humanize("true") == "yes" + assert BooleanOption.humanize("on") == "yes" - assert BooleanQuestion.humanize("no") == "no" - assert BooleanQuestion.humanize("false") == "no" - assert BooleanQuestion.humanize("off") == "no" + assert BooleanOption.humanize("no") == "no" + assert BooleanOption.humanize("false") == "no" + assert BooleanOption.humanize("off") == "no" def test_normalize_boolean_invalid(): with pytest.raises(YunohostValidationError): - BooleanQuestion.normalize("yesno") + BooleanOption.normalize("yesno") with pytest.raises(YunohostValidationError): - BooleanQuestion.normalize("foobar") + BooleanOption.normalize("foobar") with pytest.raises(YunohostValidationError): - BooleanQuestion.normalize("enabled") + BooleanOption.normalize("enabled") def test_normalize_boolean_special_yesno(): customyesno = {"yes": "enabled", "no": "disabled"} - assert BooleanQuestion.normalize("yes", customyesno) == "enabled" - assert BooleanQuestion.normalize("true", customyesno) == "enabled" - assert BooleanQuestion.normalize("enabled", customyesno) == "enabled" - assert BooleanQuestion.humanize("yes", customyesno) == "yes" - assert BooleanQuestion.humanize("true", customyesno) == "yes" - assert BooleanQuestion.humanize("enabled", customyesno) == "yes" + assert BooleanOption.normalize("yes", customyesno) == "enabled" + assert BooleanOption.normalize("true", customyesno) == "enabled" + assert BooleanOption.normalize("enabled", customyesno) == "enabled" + assert BooleanOption.humanize("yes", customyesno) == "yes" + assert BooleanOption.humanize("true", customyesno) == "yes" + assert BooleanOption.humanize("enabled", customyesno) == "yes" - assert BooleanQuestion.normalize("no", customyesno) == "disabled" - assert BooleanQuestion.normalize("false", customyesno) == "disabled" - assert BooleanQuestion.normalize("disabled", customyesno) == "disabled" - assert BooleanQuestion.humanize("no", customyesno) == "no" - assert BooleanQuestion.humanize("false", customyesno) == "no" - assert BooleanQuestion.humanize("disabled", customyesno) == "no" + assert BooleanOption.normalize("no", customyesno) == "disabled" + assert BooleanOption.normalize("false", customyesno) == "disabled" + assert BooleanOption.normalize("disabled", customyesno) == "disabled" + assert BooleanOption.humanize("no", customyesno) == "no" + assert BooleanOption.humanize("false", customyesno) == "no" + assert BooleanOption.humanize("disabled", customyesno) == "no" def test_normalize_domain(): - assert DomainQuestion.normalize("https://yolo.swag/") == "yolo.swag" - assert DomainQuestion.normalize("http://yolo.swag") == "yolo.swag" - assert DomainQuestion.normalize("yolo.swag/") == "yolo.swag" + assert DomainOption.normalize("https://yolo.swag/") == "yolo.swag" + assert DomainOption.normalize("http://yolo.swag") == "yolo.swag" + assert DomainOption.normalize("yolo.swag/") == "yolo.swag" def test_normalize_path(): - assert PathQuestion.normalize("") == "/" - assert PathQuestion.normalize("") == "/" - assert PathQuestion.normalize("macnuggets") == "/macnuggets" - assert PathQuestion.normalize("/macnuggets") == "/macnuggets" - assert PathQuestion.normalize(" /macnuggets ") == "/macnuggets" - assert PathQuestion.normalize("/macnuggets") == "/macnuggets" - assert PathQuestion.normalize("mac/nuggets") == "/mac/nuggets" - assert PathQuestion.normalize("/macnuggets/") == "/macnuggets" - assert PathQuestion.normalize("macnuggets/") == "/macnuggets" - assert PathQuestion.normalize("////macnuggets///") == "/macnuggets" + assert WebPathOption.normalize("") == "/" + assert WebPathOption.normalize("") == "/" + assert WebPathOption.normalize("macnuggets") == "/macnuggets" + assert WebPathOption.normalize("/macnuggets") == "/macnuggets" + assert WebPathOption.normalize(" /macnuggets ") == "/macnuggets" + assert WebPathOption.normalize("/macnuggets") == "/macnuggets" + assert WebPathOption.normalize("mac/nuggets") == "/mac/nuggets" + assert WebPathOption.normalize("/macnuggets/") == "/macnuggets" + assert WebPathOption.normalize("macnuggets/") == "/macnuggets" + assert WebPathOption.normalize("////macnuggets///") == "/macnuggets" def test_simple_evaluate(): diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index e50d0a3ec..c75311a56 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -37,8 +37,8 @@ from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( ARGUMENTS_TYPE_PARSERS, - FileQuestion, - Question, + FileOption, + BaseOption, ask_questions_and_parse_answers, evaluate_simple_js_expression, ) @@ -213,7 +213,7 @@ class ConfigPanel: # Read or get values and hydrate the config self._load_current_values() self._hydrate() - Question.operation_logger = operation_logger + BaseOption.operation_logger = operation_logger self._ask(action=action_id) # FIXME: here, we could want to check constrains on @@ -244,7 +244,7 @@ class ConfigPanel: # 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() + FileOption.clean_upload_dirs() # FIXME: i18n logger.success(f"Action {action_id} successful") @@ -277,7 +277,7 @@ class ConfigPanel: # Read or get values and hydrate the config self._load_current_values() self._hydrate() - Question.operation_logger = operation_logger + BaseOption.operation_logger = operation_logger self._ask() if operation_logger: @@ -305,7 +305,7 @@ class ConfigPanel: # 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() + FileOption.clean_upload_dirs() self._reload_services() diff --git a/src/utils/form.py b/src/utils/form.py index 31b3d5b87..1a1b8d47e 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -183,7 +183,7 @@ def evaluate_simple_js_expression(expr, context={}): return evaluate_simple_ast(node, context) -class Question: +class BaseOption: hide_user_input_in_prompt = False pattern: Optional[Dict] = None @@ -377,26 +377,26 @@ class Question: return self.value -class StringQuestion(Question): +class StringOption(BaseOption): argument_type = "string" default_value = "" -class EmailQuestion(StringQuestion): +class EmailOption(StringOption): pattern = { "regexp": r"^.+@.+", "error": "config_validate_email", # i18n: config_validate_email } -class URLQuestion(StringQuestion): +class URLOption(StringOption): pattern = { "regexp": r"^https?://.*$", "error": "config_validate_url", # i18n: config_validate_url } -class DateQuestion(StringQuestion): +class DateOption(StringOption): pattern = { "regexp": r"^\d{4}-\d\d-\d\d$", "error": "config_validate_date", # i18n: config_validate_date @@ -414,21 +414,21 @@ class DateQuestion(StringQuestion): raise YunohostValidationError("config_validate_date") -class TimeQuestion(StringQuestion): +class TimeOption(StringOption): pattern = { "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", "error": "config_validate_time", # i18n: config_validate_time } -class ColorQuestion(StringQuestion): +class ColorOption(StringOption): pattern = { "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", "error": "config_validate_color", # i18n: config_validate_color } -class TagsQuestion(Question): +class TagsOption(BaseOption): argument_type = "tags" default_value = "" @@ -478,7 +478,7 @@ class TagsQuestion(Question): return super()._post_parse_value() -class PasswordQuestion(Question): +class PasswordOption(BaseOption): hide_user_input_in_prompt = True argument_type = "password" default_value = "" @@ -509,13 +509,13 @@ class PasswordQuestion(Question): assert_password_is_strong_enough("user", self.value) -class PathQuestion(Question): +class WebPathOption(BaseOption): argument_type = "path" default_value = "" @staticmethod def normalize(value, option={}): - option = option.__dict__ if isinstance(option, Question) else option + option = option.__dict__ if isinstance(option, BaseOption) else option if not isinstance(value, str): raise YunohostValidationError( @@ -528,19 +528,19 @@ class PathQuestion(Question): if option.get("optional"): return "" # Hmpf here we could just have a "else" case - # but we also want PathQuestion.normalize("") to return "/" + # 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="Question is mandatory", + error="Option is mandatory", ) return "/" + value.strip().strip(" /") -class BooleanQuestion(Question): +class BooleanOption(BaseOption): argument_type = "boolean" default_value = 0 yes_answers = ["1", "yes", "y", "true", "t", "on"] @@ -548,12 +548,12 @@ class BooleanQuestion(Question): @staticmethod 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) no = option.get("no", 0) - value = BooleanQuestion.normalize(value, option) + value = BooleanOption.normalize(value, option) if value == yes: return "yes" @@ -571,7 +571,7 @@ class BooleanQuestion(Question): @staticmethod 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): value = value.strip() @@ -579,8 +579,8 @@ class BooleanQuestion(Question): technical_yes = option.get("yes", 1) technical_no = option.get("no", 0) - no_answers = BooleanQuestion.no_answers - yes_answers = BooleanQuestion.yes_answers + no_answers = BooleanOption.no_answers + yes_answers = BooleanOption.yes_answers assert ( str(technical_yes).lower() not in no_answers @@ -630,7 +630,7 @@ class BooleanQuestion(Question): return getattr(self, key, default) -class DomainQuestion(Question): +class DomainOption(BaseOption): argument_type = "domain" def __init__( @@ -661,7 +661,7 @@ class DomainQuestion(Question): return value -class AppQuestion(Question): +class AppOption(BaseOption): argument_type = "app" def __init__( @@ -688,7 +688,7 @@ class AppQuestion(Question): self.choices.update({app["id"]: _app_display(app) for app in apps}) -class UserQuestion(Question): +class UserOption(BaseOption): argument_type = "user" def __init__( @@ -721,7 +721,7 @@ class UserQuestion(Question): break -class GroupQuestion(Question): +class GroupOption(BaseOption): argument_type = "group" def __init__( @@ -747,7 +747,7 @@ class GroupQuestion(Question): self.default = "all_users" -class NumberQuestion(Question): +class NumberOption(BaseOption): argument_type = "number" default_value = None @@ -773,7 +773,7 @@ class NumberQuestion(Question): if value in [None, ""]: return None - option = option.__dict__ if isinstance(option, Question) else option + option = option.__dict__ if isinstance(option, BaseOption) else option raise YunohostValidationError( "app_argument_invalid", name=option.get("name"), @@ -800,7 +800,7 @@ class NumberQuestion(Question): ) -class DisplayTextQuestion(Question): +class DisplayTextOption(BaseOption): argument_type = "display_text" def __init__( @@ -830,7 +830,7 @@ class DisplayTextQuestion(Question): return text -class FileQuestion(Question): +class FileOption(BaseOption): argument_type = "file" upload_dirs: List[str] = [] @@ -876,7 +876,7 @@ class FileQuestion(Question): upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_") _, file_path = tempfile.mkstemp(dir=upload_dir) - FileQuestion.upload_dirs += [upload_dir] + FileOption.upload_dirs += [upload_dir] logger.debug(f"Saving file {self.name} for file question into {file_path}") @@ -895,7 +895,7 @@ class FileQuestion(Question): return self.value -class ButtonQuestion(Question): +class ButtonOption(BaseOption): argument_type = "button" enabled = None @@ -907,29 +907,29 @@ class ButtonQuestion(Question): 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, + "string": StringOption, + "text": StringOption, + "select": StringOption, + "tags": TagsOption, + "email": EmailOption, + "url": URLOption, + "date": DateOption, + "time": TimeOption, + "color": ColorOption, + "password": PasswordOption, + "path": WebPathOption, + "boolean": BooleanOption, + "domain": DomainOption, + "user": UserOption, + "group": GroupOption, + "number": NumberOption, + "range": NumberOption, + "display_text": DisplayTextOption, + "alert": DisplayTextOption, + "markdown": DisplayTextOption, + "file": FileOption, + "app": AppOption, + "button": ButtonOption, } @@ -938,7 +938,7 @@ def ask_questions_and_parse_answers( prefilled_answers: Union[str, Mapping[str, Any]] = {}, current_values: Mapping[str, Any] = {}, hooks: Dict[str, Callable[[], None]] = {}, -) -> List[Question]: +) -> List[BaseOption]: """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. From 535169823073b70dbd56e406140a329044755277 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 7 Apr 2023 17:03:03 +0200 Subject: [PATCH 2/9] form: rename ARGUMENTS_TYPE_PARSERS to OPTIONS --- src/tests/test_questions.py | 4 ++-- src/utils/configpanel.py | 10 ++++------ src/utils/form.py | 6 +++--- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 7579355bd..190eb0cba 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -15,7 +15,7 @@ from _pytest.mark.structures import ParameterSet from moulinette import Moulinette from yunohost import app, domain, user from yunohost.utils.form import ( - ARGUMENTS_TYPE_PARSERS, + OPTIONS, ask_questions_and_parse_answers, DisplayTextOption, PasswordOption, @@ -440,7 +440,7 @@ class BaseTest: 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.name == id_ assert option.ask == {"en": id_} diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index c75311a56..c4edd5259 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -36,7 +36,7 @@ from moulinette.utils.filesystem import ( from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( - ARGUMENTS_TYPE_PARSERS, + OPTIONS, FileOption, BaseOption, ask_questions_and_parse_answers, @@ -127,7 +127,7 @@ class ConfigPanel: option_type = None for _, _, option_ in self._iterate(): if option_["id"] == option: - option_type = ARGUMENTS_TYPE_PARSERS[option_["type"]] + option_type = OPTIONS[option_["type"]] break return option_type.normalize(value) if option_type else value @@ -152,7 +152,7 @@ class ConfigPanel: if mode == "full": 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 ?. option["choices"] = question_class(option).choices option["default"] = question_class(option).default @@ -160,9 +160,7 @@ class ConfigPanel: else: result[key] = {"ask": ask} if "current_value" in option: - question_class = ARGUMENTS_TYPE_PARSERS[ - option.get("type", "string") - ] + question_class = OPTIONS[option.get("type", "string")] result[key]["value"] = question_class.humanize( option["current_value"], option ) diff --git a/src/utils/form.py b/src/utils/form.py index 1a1b8d47e..82cb23afb 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -906,7 +906,7 @@ class ButtonOption(BaseOption): self.enabled = question.get("enabled", None) -ARGUMENTS_TYPE_PARSERS = { +OPTIONS = { "string": StringOption, "text": StringOption, "select": StringOption, @@ -969,7 +969,7 @@ def ask_questions_and_parse_answers( for name, raw_question in raw_questions.items(): 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) question = question_class(raw_question, context=context, hooks=hooks) if question.type == "button": @@ -996,7 +996,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: out = [] for raw_question in raw_questions: - question = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]( + question = OPTIONS[raw_question.get("type", "string")]( raw_question ) if question.choices: From 9c238f00c39022d9da083092f2c66cba947d819b Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 7 Apr 2023 17:03:40 +0200 Subject: [PATCH 3/9] form: reorder Options --- src/utils/form.py | 588 +++++++++++++++++++++++----------------------- 1 file changed, 293 insertions(+), 295 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 82cb23afb..be030a4b9 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -377,107 +377,52 @@ class BaseOption: return self.value +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) + + class StringOption(BaseOption): argument_type = "string" default_value = "" -class EmailOption(StringOption): - pattern = { - "regexp": r"^.+@.+", - "error": "config_validate_email", # i18n: config_validate_email - } - - -class URLOption(StringOption): - pattern = { - "regexp": r"^https?://.*$", - "error": "config_validate_url", # i18n: config_validate_url - } - - -class DateOption(StringOption): - 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 TimeOption(StringOption): - pattern = { - "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", - "error": "config_validate_time", # i18n: config_validate_time - } - - -class ColorOption(StringOption): - pattern = { - "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", - "error": "config_validate_color", # i18n: config_validate_color - } - - -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 _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 PasswordOption(BaseOption): hide_user_input_in_prompt = True argument_type = "password" @@ -509,35 +454,64 @@ class PasswordOption(BaseOption): assert_password_is_strong_enough("user", self.value) -class WebPathOption(BaseOption): - argument_type = "path" - default_value = "" +class ColorOption(StringOption): + pattern = { + "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", + "error": "config_validate_color", # i18n: config_validate_color + } + + +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 def normalize(value, option={}): - option = option.__dict__ if isinstance(option, BaseOption) 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 _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=option.get("name"), - error="Argument for path should be a string.", + name=self.name, + error=m18n.n("invalid_number_min", min=self.min), ) - 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(" /") + 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 BooleanOption(BaseOption): @@ -630,6 +604,191 @@ class BooleanOption(BaseOption): return getattr(self, key, default) +class DateOption(StringOption): + 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 TimeOption(StringOption): + pattern = { + "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", + "error": "config_validate_time", # i18n: config_validate_time + } + + +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 + } + + +class FileOption(BaseOption): + 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) + + 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 + + +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 _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 DomainOption(BaseOption): argument_type = "domain" @@ -747,189 +906,30 @@ class GroupOption(BaseOption): self.default = "all_users" -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 - def normalize(value, option={}): - if isinstance(value, int): - return value - - 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 _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 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 FileOption(BaseOption): - 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) - - 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 - - -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) - - OPTIONS = { + "display_text": DisplayTextOption, + "markdown": DisplayTextOption, + "alert": DisplayTextOption, + "button": ButtonOption, "string": StringOption, "text": StringOption, - "select": StringOption, - "tags": TagsOption, - "email": EmailOption, - "url": URLOption, - "date": DateOption, - "time": TimeOption, - "color": ColorOption, "password": PasswordOption, - "path": WebPathOption, - "boolean": BooleanOption, - "domain": DomainOption, - "user": UserOption, - "group": GroupOption, + "color": ColorOption, "number": NumberOption, "range": NumberOption, - "display_text": DisplayTextOption, - "alert": DisplayTextOption, - "markdown": DisplayTextOption, + "boolean": BooleanOption, + "date": DateOption, + "time": TimeOption, + "email": EmailOption, + "path": WebPathOption, + "url": URLOption, "file": FileOption, + "select": StringOption, + "tags": TagsOption, + "domain": DomainOption, "app": AppOption, - "button": ButtonOption, + "user": UserOption, + "group": GroupOption, } @@ -996,9 +996,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: out = [] for raw_question in raw_questions: - question = OPTIONS[raw_question.get("type", "string")]( - raw_question - ) + question = OPTIONS[raw_question.get("type", "string")](raw_question) if question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default From 5f4c83a4ebf722b79920f72d558c756819d05394 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 7 Apr 2023 17:44:53 +0200 Subject: [PATCH 4/9] form: rename _prevalidate() to _value_pre_validator() + _post_parse_value() to _value_post_validator() --- src/utils/form.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index be030a4b9..5640bc6bf 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -279,7 +279,7 @@ class BaseOption: try: # Normalize and validate self.value = self.normalize(self.value, self) - self._prevalidate() + self._value_pre_validator() except YunohostValidationError as e: # If in interactive cli, re-ask the current question if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1): @@ -292,7 +292,7 @@ class BaseOption: 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 post_hook = f"post_ask__{self.name}" @@ -301,7 +301,7 @@ class BaseOption: return self.values - def _prevalidate(self): + def _value_pre_validator(self): if self.value in [None, ""] and not self.optional: raise YunohostValidationError("app_argument_required", name=self.name) @@ -353,7 +353,7 @@ class BaseOption: return text_for_user_input_in_cli - def _post_parse_value(self): + def _value_post_validator(self): if not self.redact: return self.value @@ -439,8 +439,8 @@ class PasswordOption(BaseOption): "app_argument_password_no_default", name=self.name ) - def _prevalidate(self): - super()._prevalidate() + def _value_pre_validator(self): + super()._value_pre_validator() if self.value not in [None, ""]: if any(char in self.value for char in self.forbidden_chars): @@ -494,8 +494,8 @@ class NumberOption(BaseOption): error=m18n.n("invalid_number"), ) - def _prevalidate(self): - super()._prevalidate() + def _value_pre_validator(self): + super()._value_pre_validator() if self.value in [None, ""]: return @@ -610,10 +610,10 @@ class DateOption(StringOption): "error": "config_validate_date", # i18n: config_validate_date } - def _prevalidate(self): + def _value_pre_validator(self): from datetime import datetime - super()._prevalidate() + super()._value_pre_validator() if self.value not in [None, ""]: try: @@ -691,11 +691,11 @@ class FileOption(BaseOption): super().__init__(question, context, hooks) self.accept = question.get("accept", "") - def _prevalidate(self): + def _value_pre_validator(self): if self.value is None: self.value = self.current_value - super()._prevalidate() + super()._value_pre_validator() # Validation should have already failed if required if self.value in [None, ""]: @@ -711,7 +711,7 @@ class FileOption(BaseOption): error=m18n.n("file_does_not_exist", path=str(self.value)), ) - def _post_parse_value(self): + def _value_post_validator(self): from base64 import b64decode if not self.value: @@ -757,7 +757,7 @@ class TagsOption(BaseOption): value = value.strip() return value - def _prevalidate(self): + def _value_pre_validator(self): values = self.value if isinstance(values, str): values = values.split(",") @@ -780,13 +780,13 @@ class TagsOption(BaseOption): for value in values: self.value = value - super()._prevalidate() + super()._value_pre_validator() self.value = values - def _post_parse_value(self): + def _value_post_validator(self): if isinstance(self.value, list): self.value = ",".join(self.value) - return super()._post_parse_value() + return super()._value_post_validator() class DomainOption(BaseOption): From e4a0ad35ce5e54c923ba53e87154e93ec036cc06 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 7 Apr 2023 17:51:43 +0200 Subject: [PATCH 5/9] form: reorder Option methods --- src/utils/form.py | 104 +++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 5640bc6bf..4c0f15710 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -231,22 +231,6 @@ class BaseOption: value = value.strip() 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): if self.visible and not evaluate_simple_js_expression( self.visible, context=self.context @@ -301,25 +285,21 @@ class BaseOption: return self.values - 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 _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 _format_text_for_user_input_in_cli(self): text_for_user_input_in_cli = _value_for_locale(self.ask) @@ -353,6 +333,26 @@ class BaseOption: return text_for_user_input_in_cli + 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: return self.value @@ -520,6 +520,15 @@ class BooleanOption(BaseOption): yes_answers = ["1", "yes", "y", "true", "t", "on"] 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 def humanize(value, option={}): option = option.__dict__ if isinstance(option, BaseOption) else option @@ -583,14 +592,8 @@ class BooleanOption(BaseOption): choices="yes/no", ) - 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 + def get(self, key, default=None): + return getattr(self, key, default) def _format_text_for_user_input_in_cli(self): text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() @@ -600,9 +603,6 @@ class BooleanOption(BaseOption): return text_for_user_input_in_cli - def get(self, key, default=None): - return getattr(self, key, default) - class DateOption(StringOption): pattern = { @@ -678,6 +678,12 @@ 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 @@ -685,12 +691,6 @@ class FileOption(BaseOption): 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 _value_pre_validator(self): if self.value is None: self.value = self.current_value From dc99febe4c1f3e2f91e1ddac936885b9fab45a3b Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 7 Apr 2023 20:27:23 +0200 Subject: [PATCH 6/9] form: add fancy separators --- src/utils/form.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/utils/form.py b/src/utils/form.py index 4c0f15710..df70b1695 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -40,6 +40,13 @@ from yunohost.log import OperationLogger logger = getActionLogger("yunohost.form") +# ╭───────────────────────────────────────────────────────╮ +# │ ┌─╴╷ ╷╭─┐╷ │ +# │ ├─╴│╭╯├─┤│ │ +# │ ╰─╴╰╯ ╵ ╵╰─╴ │ +# ╰───────────────────────────────────────────────────────╯ + + # 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 # https://github.com/shepherdwind/simple-evaluate @@ -183,6 +190,13 @@ def evaluate_simple_js_expression(expr, context={}): return evaluate_simple_ast(node, context) +# ╭───────────────────────────────────────────────────────╮ +# │ ╭─╮┌─╮╶┬╴╶┬╴╭─╮╭╮╷╭─╴ │ +# │ │ │├─╯ │ │ │ ││││╰─╮ │ +# │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + class BaseOption: hide_user_input_in_prompt = False pattern: Optional[Dict] = None @@ -377,6 +391,11 @@ class BaseOption: return self.value +# ╭───────────────────────────────────────────────────────╮ +# │ DISPLAY OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + class DisplayTextOption(BaseOption): argument_type = "display_text" @@ -418,6 +437,14 @@ class ButtonOption(BaseOption): self.enabled = question.get("enabled", None) +# ╭───────────────────────────────────────────────────────╮ +# │ INPUT OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +# ─ STRINGS ─────────────────────────────────────────────── + + class StringOption(BaseOption): argument_type = "string" default_value = "" @@ -461,6 +488,9 @@ class ColorOption(StringOption): } +# ─ NUMERIC ─────────────────────────────────────────────── + + class NumberOption(BaseOption): argument_type = "number" default_value = None @@ -514,6 +544,9 @@ class NumberOption(BaseOption): ) +# ─ BOOLEAN ─────────────────────────────────────────────── + + class BooleanOption(BaseOption): argument_type = "boolean" default_value = 0 @@ -604,6 +637,9 @@ class BooleanOption(BaseOption): return text_for_user_input_in_cli +# ─ TIME ────────────────────────────────────────────────── + + class DateOption(StringOption): pattern = { "regexp": r"^\d{4}-\d\d-\d\d$", @@ -629,6 +665,9 @@ class TimeOption(StringOption): } +# ─ LOCATIONS ───────────────────────────────────────────── + + class EmailOption(StringOption): pattern = { "regexp": r"^.+@.+", @@ -674,6 +713,9 @@ class URLOption(StringOption): } +# ─ FILE ────────────────────────────────────────────────── + + class FileOption(BaseOption): argument_type = "file" upload_dirs: List[str] = [] @@ -739,6 +781,9 @@ class FileOption(BaseOption): return self.value +# ─ CHOICES ─────────────────────────────────────────────── + + class TagsOption(BaseOption): argument_type = "tags" default_value = "" @@ -933,6 +978,13 @@ OPTIONS = { } +# ╭───────────────────────────────────────────────────────╮ +# │ ╷ ╷╶┬╴╶┬╴╷ ╭─╴ │ +# │ │ │ │ │ │ ╰─╮ │ +# │ ╰─╯ ╵ ╶┴╴╰─╴╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + def ask_questions_and_parse_answers( raw_questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}, From 67687b7cff0363590c3cbd92eb09478a79f27bc6 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 7 Apr 2023 21:15:06 +0200 Subject: [PATCH 7/9] configpanel: reorder ConfigPanel methods --- src/app.py | 6 +- src/domain.py | 154 ++++++++++++++++++------------------ src/settings.py | 154 ++++++++++++++++++------------------ src/utils/configpanel.py | 164 +++++++++++++++++++-------------------- 4 files changed, 239 insertions(+), 239 deletions(-) diff --git a/src/app.py b/src/app.py index 96225e7b2..604fd9acb 100644 --- a/src/app.py +++ b/src/app.py @@ -1878,13 +1878,13 @@ class AppConfigPanel(ConfigPanel): 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") - def _load_current_values(self): - self.values = self._call_config_script("show") - def _run_action(self, action): env = {key: str(value) for key, value in self.new_values.items()} self._call_config_script(action, env=env) + def _load_current_values(self): + self.values = self._call_config_script("show") + def _apply(self): env = {key: str(value) for key, value in self.new_values.items()} return_content = self._call_config_script("apply", env=env) diff --git a/src/domain.py b/src/domain.py index 498c2417a..d2997ab59 100644 --- a/src/domain.py +++ b/src/domain.py @@ -538,6 +538,83 @@ class DomainConfigPanel(ConfigPanel): save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml" 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_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 _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 _apply(self): if ( "default_app" in self.future_values @@ -586,83 +663,6 @@ class DomainConfigPanel(ConfigPanel): if 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): import urllib.parse diff --git a/src/settings.py b/src/settings.py index f863ef74d..26da14866 100644 --- a/src/settings.py +++ b/src/settings.py @@ -125,83 +125,6 @@ class SettingsConfigPanel(ConfigPanel): def __init__(self, config_path=None, save_path=None, creation=False): super().__init__("settings") - def _apply(self): - root_password = self.new_values.pop("root_password", None) - root_password_confirm = self.new_values.pop("root_password_confirm", None) - passwordless_sudo = self.new_values.pop("passwordless_sudo", None) - - self.values = { - k: v for k, v in self.values.items() if k not in self.virtual_settings - } - self.new_values = { - k: v for k, v in self.new_values.items() if k not in self.virtual_settings - } - - assert all(v not in self.future_values for v in self.virtual_settings) - - if root_password and root_password.strip(): - if root_password != root_password_confirm: - raise YunohostValidationError("password_confirmation_not_the_same") - - from yunohost.tools import tools_rootpw - - tools_rootpw(root_password, check_strength=True) - - if passwordless_sudo is not None: - from yunohost.utils.ldap import _get_ldap_interface - - ldap = _get_ldap_interface() - ldap.update( - "cn=admins,ou=sudo", - {"sudoOption": ["!authenticate"] if passwordless_sudo else []}, - ) - - super()._apply() - - settings = { - k: v for k, v in self.future_values.items() if self.values.get(k) != v - } - for setting_name, value in settings.items(): - try: - trigger_post_change_hook( - setting_name, self.values.get(setting_name), value - ) - except Exception as e: - logger.error(f"Post-change hook for setting failed : {e}") - 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) @@ -257,6 +180,83 @@ class SettingsConfigPanel(ConfigPanel): logger.success(m18n.n("global_settings_reset_success")) operation_logger.success() + 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 _apply(self): + root_password = self.new_values.pop("root_password", None) + root_password_confirm = self.new_values.pop("root_password_confirm", None) + passwordless_sudo = self.new_values.pop("passwordless_sudo", None) + + self.values = { + k: v for k, v in self.values.items() if k not in self.virtual_settings + } + self.new_values = { + k: v for k, v in self.new_values.items() if k not in self.virtual_settings + } + + assert all(v not in self.future_values for v in self.virtual_settings) + + if root_password and root_password.strip(): + if root_password != root_password_confirm: + raise YunohostValidationError("password_confirmation_not_the_same") + + from yunohost.tools import tools_rootpw + + tools_rootpw(root_password, check_strength=True) + + if passwordless_sudo is not None: + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + ldap.update( + "cn=admins,ou=sudo", + {"sudoOption": ["!authenticate"] if passwordless_sudo else []}, + ) + + super()._apply() + + settings = { + k: v for k, v in self.future_values.items() if self.values.get(k) != v + } + for setting_name, value in settings.items(): + try: + trigger_post_change_hook( + setting_name, self.values.get(setting_name), value + ) + except Exception as e: + logger.error(f"Post-change hook for setting failed : {e}") + raise + # Meant to be a dict of setting_name -> function to call post_change_hooks = {} diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index c4edd5259..50380aad5 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -175,6 +175,68 @@ class ConfigPanel: else: 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._load_current_values() + 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): actions = {} @@ -248,68 +310,6 @@ class ConfigPanel: logger.success(f"Action {action_id} successful") operation_logger.success() - 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._load_current_values() - 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 _get_toml(self): return read_toml(self.config_path) @@ -488,6 +488,26 @@ class ConfigPanel: return self.config + def _get_default_values(self): + return { + option["id"]: option["default"] + for _, _, option in self._iterate() + if "default" in option + } + + 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 _hydrate(self): # Hydrating config panel with current value for _, section, option in self._iterate(): @@ -604,13 +624,6 @@ class ConfigPanel: } ) - def _get_default_values(self): - return { - option["id"]: option["default"] - for _, _, option in self._iterate() - if "default" in option - } - @property def future_values(self): return {**self.values, **self.new_values} @@ -624,19 +637,6 @@ class ConfigPanel: 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): args = urllib.parse.parse_qs(args or "", keep_blank_values=True) self.args = {key: ",".join(value_) for key, value_ in args.items()} From ba320781808cb1923e5467d9847edec4f5791fc4 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sat, 8 Apr 2023 14:09:11 +0200 Subject: [PATCH 8/9] configpanel: rename data methods --- src/app.py | 2 +- src/domain.py | 16 ++++++++-------- src/settings.py | 8 ++++---- src/utils/configpanel.py | 12 ++++++------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/app.py b/src/app.py index 604fd9acb..97227ed0c 100644 --- a/src/app.py +++ b/src/app.py @@ -1882,7 +1882,7 @@ class AppConfigPanel(ConfigPanel): env = {key: str(value) for key, value in self.new_values.items()} self._call_config_script(action, env=env) - def _load_current_values(self): + def _get_raw_settings(self): self.values = self._call_config_script("show") def _apply(self): diff --git a/src/domain.py b/src/domain.py index d2997ab59..4f96d08c4 100644 --- a/src/domain.py +++ b/src/domain.py @@ -555,8 +555,8 @@ class DomainConfigPanel(ConfigPanel): return result - def _get_toml(self): - toml = super()._get_toml() + def _get_raw_config(self): + toml = super()._get_raw_config() toml["feature"]["xmpp"]["xmpp"]["default"] = ( 1 if self.entity == _get_maindomain() else 0 @@ -571,7 +571,7 @@ class DomainConfigPanel(ConfigPanel): 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 ... + # 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"] @@ -594,21 +594,21 @@ class DomainConfigPanel(ConfigPanel): f"domain_config_cert_summary_{status['summary']}" ) - # FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ... + # FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ... self.cert_status = status return toml - def _load_current_values(self): + def _get_raw_settings(self): # TODO add mechanism to share some settings with other domains on the same zone - super()._load_current_values() + super()._get_raw_settings() - # FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ... + # 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 _load_current_values ... + # 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"] diff --git a/src/settings.py b/src/settings.py index 26da14866..6690ab3fd 100644 --- a/src/settings.py +++ b/src/settings.py @@ -180,8 +180,8 @@ class SettingsConfigPanel(ConfigPanel): logger.success(m18n.n("global_settings_reset_success")) operation_logger.success() - def _get_toml(self): - toml = super()._get_toml() + def _get_raw_config(self): + toml = super()._get_raw_config() # Dynamic choice list for portal themes THEMEDIR = "/usr/share/ssowat/portal/assets/themes/" @@ -193,8 +193,8 @@ class SettingsConfigPanel(ConfigPanel): return toml - def _load_current_values(self): - super()._load_current_values() + 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 diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 50380aad5..fcdaea193 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -116,7 +116,7 @@ class ConfigPanel: raise YunohostValidationError("config_no_panel") # Read or get values and hydrate the config - self._load_current_values() + self._get_raw_settings() self._hydrate() # In 'classic' mode, we display the current value if key refer to an option @@ -200,7 +200,7 @@ class ConfigPanel: self._parse_pre_answered(args, value, args_file) # Read or get values and hydrate the config - self._load_current_values() + self._get_raw_settings() self._hydrate() BaseOption.operation_logger = operation_logger self._ask() @@ -271,7 +271,7 @@ class ConfigPanel: self._parse_pre_answered(args, None, args_file) # Read or get values and hydrate the config - self._load_current_values() + self._get_raw_settings() self._hydrate() BaseOption.operation_logger = operation_logger self._ask(action=action_id) @@ -310,7 +310,7 @@ class ConfigPanel: logger.success(f"Action {action_id} successful") operation_logger.success() - def _get_toml(self): + def _get_raw_config(self): return read_toml(self.config_path) def _get_config_panel(self): @@ -326,7 +326,7 @@ class ConfigPanel: logger.debug(f"Config panel {self.config_path} doesn't exists") return None - toml_config_panel = self._get_toml() + toml_config_panel = self._get_raw_config() # Check TOML config panel is in a supported version if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED: @@ -495,7 +495,7 @@ class ConfigPanel: if "default" in option } - def _load_current_values(self): + def _get_raw_settings(self): """ Retrieve entries in YAML file And set default values if needed From fe5c73b4eded8483ee37ea041f5f867f22c5ced7 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 10 Apr 2023 17:47:19 +0200 Subject: [PATCH 9/9] form+configpanel: sort imports --- src/utils/configpanel.py | 14 ++++---------- src/utils/form.py | 24 ++++++++++-------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index fcdaea193..2c56eb754 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -23,25 +23,19 @@ import urllib.parse from collections import OrderedDict from typing import Union -from moulinette.interfaces.cli import colorize 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.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.form import ( OPTIONS, - FileOption, BaseOption, + FileOption, ask_questions_and_parse_answers, evaluate_simple_js_expression, ) +from yunohost.utils.i18n import _value_for_locale logger = getActionLogger("yunohost.configpanel") CONFIG_PANEL_VERSION_SUPPORTED = 1.0 diff --git a/src/utils/form.py b/src/utils/form.py index df70b1695..12c3249c3 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -16,26 +16,22 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -import os -import re -import urllib.parse -import tempfile -import shutil import ast 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.interfaces.cli import colorize +from moulinette.utils.filesystem import read_file, write_to_file 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.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.i18n import _value_for_locale logger = getActionLogger("yunohost.form")