form: use Enum for Option's type

This commit is contained in:
axolotle 2023-04-13 15:10:23 +02:00
parent 1c7d427be0
commit e87f8ef93a
2 changed files with 118 additions and 61 deletions

View file

@ -34,6 +34,7 @@ from yunohost.utils.form import (
BaseInputOption, BaseInputOption,
BaseOption, BaseOption,
FileOption, FileOption,
OptionType,
ask_questions_and_parse_answers, ask_questions_and_parse_answers,
evaluate_simple_js_expression, evaluate_simple_js_expression,
) )
@ -148,7 +149,7 @@ class ConfigPanel:
if mode == "full": if mode == "full":
option["ask"] = ask option["ask"] = ask
question_class = OPTIONS[option.get("type", "string")] question_class = OPTIONS[option.get("type", OptionType.string)]
# FIXME : maybe other properties should be taken from the question, not just choices ?. # FIXME : maybe other properties should be taken from the question, not just choices ?.
if issubclass(question_class, BaseChoicesOption): if issubclass(question_class, BaseChoicesOption):
option["choices"] = question_class(option).choices option["choices"] = question_class(option).choices
@ -158,7 +159,7 @@ class ConfigPanel:
else: else:
result[key] = {"ask": ask} result[key] = {"ask": ask}
if "current_value" in option: if "current_value" in option:
question_class = OPTIONS[option.get("type", "string")] question_class = OPTIONS[option.get("type", OptionType.string)]
result[key]["value"] = question_class.humanize( result[key]["value"] = question_class.humanize(
option["current_value"], option option["current_value"], option
) )
@ -243,7 +244,7 @@ class ConfigPanel:
self.filter_key = "" self.filter_key = ""
self._get_config_panel() self._get_config_panel()
for panel, section, option in self._iterate(): for panel, section, option in self._iterate():
if option["type"] == "button": if option["type"] == OptionType.button:
key = f"{panel['id']}.{section['id']}.{option['id']}" key = f"{panel['id']}.{section['id']}.{option['id']}"
actions[key] = _value_for_locale(option["ask"]) actions[key] = _value_for_locale(option["ask"])
@ -425,7 +426,7 @@ class ConfigPanel:
subnode["name"] = key # legacy subnode["name"] = key # legacy
subnode.setdefault("optional", raw_infos.get("optional", True)) subnode.setdefault("optional", raw_infos.get("optional", True))
# If this section contains at least one button, it becomes an "action" section # If this section contains at least one button, it becomes an "action" section
if subnode.get("type") == "button": if subnode.get("type") == OptionType.button:
out["is_action_section"] = True out["is_action_section"] = True
out.setdefault(sublevel, []).append(subnode) out.setdefault(sublevel, []).append(subnode)
# Key/value are a property # Key/value are a property
@ -500,13 +501,13 @@ class ConfigPanel:
# Hydrating config panel with current value # Hydrating config panel with current value
for _, section, option in self._iterate(): for _, section, option in self._iterate():
if option["id"] not in self.values: if option["id"] not in self.values:
allowed_empty_types = [ allowed_empty_types = {
"alert", OptionType.alert,
"display_text", OptionType.display_text,
"markdown", OptionType.markdown,
"file", OptionType.file,
"button", OptionType.button,
] }
if section["is_action_section"] and option.get("default") is not None: if section["is_action_section"] and option.get("default") is not None:
self.values[option["id"]] = option["default"] self.values[option["id"]] = option["default"]
@ -587,7 +588,7 @@ class ConfigPanel:
section["options"] = [ section["options"] = [
option option
for option in section["options"] for option in section["options"]
if option.get("type", "string") != "button" if option.get("type", OptionType.string) != OptionType.button
or option["id"] == action or option["id"] == action
] ]

View file

@ -23,7 +23,8 @@ import re
import shutil import shutil
import tempfile import tempfile
import urllib.parse import urllib.parse
from typing import Any, Callable, Dict, List, Mapping, Optional, Union from enum import Enum
from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Union
from moulinette import Moulinette, m18n from moulinette import Moulinette, m18n
from moulinette.interfaces.cli import colorize from moulinette.interfaces.cli import colorize
@ -193,7 +194,50 @@ def evaluate_simple_js_expression(expr, context={}):
# │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │ # │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │
# ╰───────────────────────────────────────────────────────╯ # ╰───────────────────────────────────────────────────────╯
FORBIDDEN_READONLY_TYPES = {"password", "app", "domain", "user", "group"}
class OptionType(str, Enum):
# display
display_text = "display_text"
markdown = "markdown"
alert = "alert"
# action
button = "button"
# text
string = "string"
text = "text"
password = "password"
color = "color"
# numeric
number = "number"
range = "range"
# boolean
boolean = "boolean"
# time
date = "date"
time = "time"
# location
email = "email"
path = "path"
url = "url"
# file
file = "file"
# choice
select = "select"
tags = "tags"
# entity
domain = "domain"
app = "app"
user = "user"
group = "group"
FORBIDDEN_READONLY_TYPES = {
OptionType.password,
OptionType.app,
OptionType.domain,
OptionType.user,
OptionType.group,
}
class BaseOption: class BaseOption:
@ -202,7 +246,7 @@ class BaseOption:
question: Dict[str, Any], question: Dict[str, Any],
): ):
self.name = question["name"] self.name = question["name"]
self.type = question.get("type", "string") self.type = question.get("type", OptionType.string)
self.visible = question.get("visible", True) self.visible = question.get("visible", True)
self.readonly = question.get("readonly", False) self.readonly = question.get("readonly", False)
@ -240,15 +284,15 @@ class BaseReadonlyOption(BaseOption):
class DisplayTextOption(BaseReadonlyOption): class DisplayTextOption(BaseReadonlyOption):
argument_type = "display_text" type: Literal[OptionType.display_text] = OptionType.display_text
class MarkdownOption(BaseReadonlyOption): class MarkdownOption(BaseReadonlyOption):
argument_type = "markdown" type: Literal[OptionType.markdown] = OptionType.markdown
class AlertOption(BaseReadonlyOption): class AlertOption(BaseReadonlyOption):
argument_type = "alert" type: Literal[OptionType.alert] = OptionType.alert
def __init__(self, question): def __init__(self, question):
super().__init__(question) super().__init__(question)
@ -271,7 +315,7 @@ class AlertOption(BaseReadonlyOption):
class ButtonOption(BaseReadonlyOption): class ButtonOption(BaseReadonlyOption):
argument_type = "button" type: Literal[OptionType.button] = OptionType.button
enabled = True enabled = True
def __init__(self, question): def __init__(self, question):
@ -373,14 +417,21 @@ class BaseInputOption(BaseOption):
# ─ STRINGS ─────────────────────────────────────────────── # ─ STRINGS ───────────────────────────────────────────────
class StringOption(BaseInputOption): class BaseStringOption(BaseInputOption):
argument_type = "string"
default_value = "" default_value = ""
class StringOption(BaseStringOption):
type: Literal[OptionType.string] = OptionType.string
class TextOption(BaseStringOption):
type: Literal[OptionType.text] = OptionType.text
class PasswordOption(BaseInputOption): class PasswordOption(BaseInputOption):
type: Literal[OptionType.password] = OptionType.password
hide_user_input_in_prompt = True hide_user_input_in_prompt = True
argument_type = "password"
default_value = "" default_value = ""
forbidden_chars = "{}" forbidden_chars = "{}"
@ -407,7 +458,8 @@ class PasswordOption(BaseInputOption):
assert_password_is_strong_enough("user", self.value) assert_password_is_strong_enough("user", self.value)
class ColorOption(StringOption): class ColorOption(BaseStringOption):
type: Literal[OptionType.color] = OptionType.color
pattern = { pattern = {
"regexp": r"^#[ABCDEFabcdef\d]{3,6}$", "regexp": r"^#[ABCDEFabcdef\d]{3,6}$",
"error": "config_validate_color", # i18n: config_validate_color "error": "config_validate_color", # i18n: config_validate_color
@ -418,7 +470,7 @@ class ColorOption(StringOption):
class NumberOption(BaseInputOption): class NumberOption(BaseInputOption):
argument_type = "number" type: Literal[OptionType.number, OptionType.range] = OptionType.number
default_value = None default_value = None
def __init__(self, question): def __init__(self, question):
@ -472,7 +524,7 @@ class NumberOption(BaseInputOption):
class BooleanOption(BaseInputOption): class BooleanOption(BaseInputOption):
argument_type = "boolean" type: Literal[OptionType.boolean] = OptionType.boolean
default_value = 0 default_value = 0
yes_answers = ["1", "yes", "y", "true", "t", "on"] yes_answers = ["1", "yes", "y", "true", "t", "on"]
no_answers = ["0", "no", "n", "false", "f", "off"] no_answers = ["0", "no", "n", "false", "f", "off"]
@ -562,7 +614,8 @@ class BooleanOption(BaseInputOption):
# ─ TIME ────────────────────────────────────────────────── # ─ TIME ──────────────────────────────────────────────────
class DateOption(StringOption): class DateOption(BaseStringOption):
type: Literal[OptionType.date] = OptionType.date
pattern = { pattern = {
"regexp": r"^\d{4}-\d\d-\d\d$", "regexp": r"^\d{4}-\d\d-\d\d$",
"error": "config_validate_date", # i18n: config_validate_date "error": "config_validate_date", # i18n: config_validate_date
@ -580,7 +633,8 @@ class DateOption(StringOption):
raise YunohostValidationError("config_validate_date") raise YunohostValidationError("config_validate_date")
class TimeOption(StringOption): class TimeOption(BaseStringOption):
type: Literal[OptionType.time] = OptionType.time
pattern = { pattern = {
"regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$",
"error": "config_validate_time", # i18n: config_validate_time "error": "config_validate_time", # i18n: config_validate_time
@ -590,7 +644,8 @@ class TimeOption(StringOption):
# ─ LOCATIONS ───────────────────────────────────────────── # ─ LOCATIONS ─────────────────────────────────────────────
class EmailOption(StringOption): class EmailOption(BaseStringOption):
type: Literal[OptionType.email] = OptionType.email
pattern = { pattern = {
"regexp": r"^.+@.+", "regexp": r"^.+@.+",
"error": "config_validate_email", # i18n: config_validate_email "error": "config_validate_email", # i18n: config_validate_email
@ -598,7 +653,7 @@ class EmailOption(StringOption):
class WebPathOption(BaseInputOption): class WebPathOption(BaseInputOption):
argument_type = "path" type: Literal[OptionType.path] = OptionType.path
default_value = "" default_value = ""
@staticmethod @staticmethod
@ -628,7 +683,8 @@ class WebPathOption(BaseInputOption):
return "/" + value.strip().strip(" /") return "/" + value.strip().strip(" /")
class URLOption(StringOption): class URLOption(BaseStringOption):
type: Literal[OptionType.url] = OptionType.url
pattern = { pattern = {
"regexp": r"^https?://.*$", "regexp": r"^https?://.*$",
"error": "config_validate_url", # i18n: config_validate_url "error": "config_validate_url", # i18n: config_validate_url
@ -639,7 +695,7 @@ class URLOption(StringOption):
class FileOption(BaseInputOption): class FileOption(BaseInputOption):
argument_type = "file" type: Literal[OptionType.file] = OptionType.file
upload_dirs: List[str] = [] upload_dirs: List[str] = []
def __init__(self, question): def __init__(self, question):
@ -764,12 +820,12 @@ class BaseChoicesOption(BaseInputOption):
class SelectOption(BaseChoicesOption): class SelectOption(BaseChoicesOption):
argument_type = "select" type: Literal[OptionType.select] = OptionType.select
default_value = "" default_value = ""
class TagsOption(BaseChoicesOption): class TagsOption(BaseChoicesOption):
argument_type = "tags" type: Literal[OptionType.tags] = OptionType.tags
default_value = "" default_value = ""
@staticmethod @staticmethod
@ -822,7 +878,7 @@ class TagsOption(BaseChoicesOption):
class DomainOption(BaseChoicesOption): class DomainOption(BaseChoicesOption):
argument_type = "domain" type: Literal[OptionType.domain] = OptionType.domain
def __init__(self, question): def __init__(self, question):
from yunohost.domain import domain_list, _get_maindomain from yunohost.domain import domain_list, _get_maindomain
@ -851,7 +907,7 @@ class DomainOption(BaseChoicesOption):
class AppOption(BaseChoicesOption): class AppOption(BaseChoicesOption):
argument_type = "app" type: Literal[OptionType.app] = OptionType.app
def __init__(self, question): def __init__(self, question):
from yunohost.app import app_list from yunohost.app import app_list
@ -877,7 +933,7 @@ class AppOption(BaseChoicesOption):
class UserOption(BaseChoicesOption): class UserOption(BaseChoicesOption):
argument_type = "user" type: Literal[OptionType.user] = OptionType.user
def __init__(self, question): def __init__(self, question):
from yunohost.user import user_list, user_info from yunohost.user import user_list, user_info
@ -908,7 +964,7 @@ class UserOption(BaseChoicesOption):
class GroupOption(BaseChoicesOption): class GroupOption(BaseChoicesOption):
argument_type = "group" type: Literal[OptionType.group] = OptionType.group
def __init__(self, question): def __init__(self, question):
from yunohost.user import user_group_list from yunohost.user import user_group_list
@ -932,29 +988,29 @@ class GroupOption(BaseChoicesOption):
OPTIONS = { OPTIONS = {
"display_text": DisplayTextOption, OptionType.display_text: DisplayTextOption,
"markdown": MarkdownOption, OptionType.markdown: MarkdownOption,
"alert": AlertOption, OptionType.alert: AlertOption,
"button": ButtonOption, OptionType.button: ButtonOption,
"string": StringOption, OptionType.string: StringOption,
"text": StringOption, OptionType.text: StringOption,
"password": PasswordOption, OptionType.password: PasswordOption,
"color": ColorOption, OptionType.color: ColorOption,
"number": NumberOption, OptionType.number: NumberOption,
"range": NumberOption, OptionType.range: NumberOption,
"boolean": BooleanOption, OptionType.boolean: BooleanOption,
"date": DateOption, OptionType.date: DateOption,
"time": TimeOption, OptionType.time: TimeOption,
"email": EmailOption, OptionType.email: EmailOption,
"path": WebPathOption, OptionType.path: WebPathOption,
"url": URLOption, OptionType.url: URLOption,
"file": FileOption, OptionType.file: FileOption,
"select": SelectOption, OptionType.select: SelectOption,
"tags": TagsOption, OptionType.tags: TagsOption,
"domain": DomainOption, OptionType.domain: DomainOption,
"app": AppOption, OptionType.app: AppOption,
"user": UserOption, OptionType.user: UserOption,
"group": GroupOption, OptionType.group: GroupOption,
} }
@ -1122,7 +1178,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List:
out = [] out = []
for raw_question in raw_questions: for raw_question in raw_questions:
question = OPTIONS[raw_question.get("type", "string")](raw_question) question = OPTIONS[raw_question.get("type", OptionType.string)](raw_question)
if isinstance(question, BaseChoicesOption) and question.choices: if isinstance(question, BaseChoicesOption) and question.choices:
raw_question["choices"] = question.choices raw_question["choices"] = question.choices
raw_question["default"] = question.default raw_question["default"] = question.default