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.