mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
form: use Enum for Option's type
This commit is contained in:
parent
1c7d427be0
commit
e87f8ef93a
2 changed files with 118 additions and 61 deletions
|
@ -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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue