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,
|
||||
BaseOption,
|
||||
FileOption,
|
||||
OptionType,
|
||||
ask_questions_and_parse_answers,
|
||||
evaluate_simple_js_expression,
|
||||
)
|
||||
|
@ -148,7 +149,7 @@ class ConfigPanel:
|
|||
|
||||
if mode == "full":
|
||||
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 ?.
|
||||
if issubclass(question_class, BaseChoicesOption):
|
||||
option["choices"] = question_class(option).choices
|
||||
|
@ -158,7 +159,7 @@ class ConfigPanel:
|
|||
else:
|
||||
result[key] = {"ask": ask}
|
||||
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(
|
||||
option["current_value"], option
|
||||
)
|
||||
|
@ -243,7 +244,7 @@ class ConfigPanel:
|
|||
self.filter_key = ""
|
||||
self._get_config_panel()
|
||||
for panel, section, option in self._iterate():
|
||||
if option["type"] == "button":
|
||||
if option["type"] == OptionType.button:
|
||||
key = f"{panel['id']}.{section['id']}.{option['id']}"
|
||||
actions[key] = _value_for_locale(option["ask"])
|
||||
|
||||
|
@ -425,7 +426,7 @@ class ConfigPanel:
|
|||
subnode["name"] = key # legacy
|
||||
subnode.setdefault("optional", raw_infos.get("optional", True))
|
||||
# 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.setdefault(sublevel, []).append(subnode)
|
||||
# Key/value are a property
|
||||
|
@ -500,13 +501,13 @@ class ConfigPanel:
|
|||
# Hydrating config panel with current value
|
||||
for _, section, option in self._iterate():
|
||||
if option["id"] not in self.values:
|
||||
allowed_empty_types = [
|
||||
"alert",
|
||||
"display_text",
|
||||
"markdown",
|
||||
"file",
|
||||
"button",
|
||||
]
|
||||
allowed_empty_types = {
|
||||
OptionType.alert,
|
||||
OptionType.display_text,
|
||||
OptionType.markdown,
|
||||
OptionType.file,
|
||||
OptionType.button,
|
||||
}
|
||||
|
||||
if section["is_action_section"] and option.get("default") is not None:
|
||||
self.values[option["id"]] = option["default"]
|
||||
|
@ -587,7 +588,7 @@ class ConfigPanel:
|
|||
section["options"] = [
|
||||
option
|
||||
for option in section["options"]
|
||||
if option.get("type", "string") != "button"
|
||||
if option.get("type", OptionType.string) != OptionType.button
|
||||
or option["id"] == action
|
||||
]
|
||||
|
||||
|
|
|
@ -23,7 +23,8 @@ import re
|
|||
import shutil
|
||||
import tempfile
|
||||
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.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:
|
||||
|
@ -202,7 +246,7 @@ class BaseOption:
|
|||
question: Dict[str, Any],
|
||||
):
|
||||
self.name = question["name"]
|
||||
self.type = question.get("type", "string")
|
||||
self.type = question.get("type", OptionType.string)
|
||||
self.visible = question.get("visible", True)
|
||||
|
||||
self.readonly = question.get("readonly", False)
|
||||
|
@ -240,15 +284,15 @@ class BaseReadonlyOption(BaseOption):
|
|||
|
||||
|
||||
class DisplayTextOption(BaseReadonlyOption):
|
||||
argument_type = "display_text"
|
||||
type: Literal[OptionType.display_text] = OptionType.display_text
|
||||
|
||||
|
||||
class MarkdownOption(BaseReadonlyOption):
|
||||
argument_type = "markdown"
|
||||
type: Literal[OptionType.markdown] = OptionType.markdown
|
||||
|
||||
|
||||
class AlertOption(BaseReadonlyOption):
|
||||
argument_type = "alert"
|
||||
type: Literal[OptionType.alert] = OptionType.alert
|
||||
|
||||
def __init__(self, question):
|
||||
super().__init__(question)
|
||||
|
@ -271,7 +315,7 @@ class AlertOption(BaseReadonlyOption):
|
|||
|
||||
|
||||
class ButtonOption(BaseReadonlyOption):
|
||||
argument_type = "button"
|
||||
type: Literal[OptionType.button] = OptionType.button
|
||||
enabled = True
|
||||
|
||||
def __init__(self, question):
|
||||
|
@ -373,14 +417,21 @@ class BaseInputOption(BaseOption):
|
|||
# ─ STRINGS ───────────────────────────────────────────────
|
||||
|
||||
|
||||
class StringOption(BaseInputOption):
|
||||
argument_type = "string"
|
||||
class BaseStringOption(BaseInputOption):
|
||||
default_value = ""
|
||||
|
||||
|
||||
class StringOption(BaseStringOption):
|
||||
type: Literal[OptionType.string] = OptionType.string
|
||||
|
||||
|
||||
class TextOption(BaseStringOption):
|
||||
type: Literal[OptionType.text] = OptionType.text
|
||||
|
||||
|
||||
class PasswordOption(BaseInputOption):
|
||||
type: Literal[OptionType.password] = OptionType.password
|
||||
hide_user_input_in_prompt = True
|
||||
argument_type = "password"
|
||||
default_value = ""
|
||||
forbidden_chars = "{}"
|
||||
|
||||
|
@ -407,7 +458,8 @@ class PasswordOption(BaseInputOption):
|
|||
assert_password_is_strong_enough("user", self.value)
|
||||
|
||||
|
||||
class ColorOption(StringOption):
|
||||
class ColorOption(BaseStringOption):
|
||||
type: Literal[OptionType.color] = OptionType.color
|
||||
pattern = {
|
||||
"regexp": r"^#[ABCDEFabcdef\d]{3,6}$",
|
||||
"error": "config_validate_color", # i18n: config_validate_color
|
||||
|
@ -418,7 +470,7 @@ class ColorOption(StringOption):
|
|||
|
||||
|
||||
class NumberOption(BaseInputOption):
|
||||
argument_type = "number"
|
||||
type: Literal[OptionType.number, OptionType.range] = OptionType.number
|
||||
default_value = None
|
||||
|
||||
def __init__(self, question):
|
||||
|
@ -472,7 +524,7 @@ class NumberOption(BaseInputOption):
|
|||
|
||||
|
||||
class BooleanOption(BaseInputOption):
|
||||
argument_type = "boolean"
|
||||
type: Literal[OptionType.boolean] = OptionType.boolean
|
||||
default_value = 0
|
||||
yes_answers = ["1", "yes", "y", "true", "t", "on"]
|
||||
no_answers = ["0", "no", "n", "false", "f", "off"]
|
||||
|
@ -562,7 +614,8 @@ class BooleanOption(BaseInputOption):
|
|||
# ─ TIME ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class DateOption(StringOption):
|
||||
class DateOption(BaseStringOption):
|
||||
type: Literal[OptionType.date] = OptionType.date
|
||||
pattern = {
|
||||
"regexp": r"^\d{4}-\d\d-\d\d$",
|
||||
"error": "config_validate_date", # i18n: config_validate_date
|
||||
|
@ -580,7 +633,8 @@ class DateOption(StringOption):
|
|||
raise YunohostValidationError("config_validate_date")
|
||||
|
||||
|
||||
class TimeOption(StringOption):
|
||||
class TimeOption(BaseStringOption):
|
||||
type: Literal[OptionType.time] = OptionType.time
|
||||
pattern = {
|
||||
"regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$",
|
||||
"error": "config_validate_time", # i18n: config_validate_time
|
||||
|
@ -590,7 +644,8 @@ class TimeOption(StringOption):
|
|||
# ─ LOCATIONS ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class EmailOption(StringOption):
|
||||
class EmailOption(BaseStringOption):
|
||||
type: Literal[OptionType.email] = OptionType.email
|
||||
pattern = {
|
||||
"regexp": r"^.+@.+",
|
||||
"error": "config_validate_email", # i18n: config_validate_email
|
||||
|
@ -598,7 +653,7 @@ class EmailOption(StringOption):
|
|||
|
||||
|
||||
class WebPathOption(BaseInputOption):
|
||||
argument_type = "path"
|
||||
type: Literal[OptionType.path] = OptionType.path
|
||||
default_value = ""
|
||||
|
||||
@staticmethod
|
||||
|
@ -628,7 +683,8 @@ class WebPathOption(BaseInputOption):
|
|||
return "/" + value.strip().strip(" /")
|
||||
|
||||
|
||||
class URLOption(StringOption):
|
||||
class URLOption(BaseStringOption):
|
||||
type: Literal[OptionType.url] = OptionType.url
|
||||
pattern = {
|
||||
"regexp": r"^https?://.*$",
|
||||
"error": "config_validate_url", # i18n: config_validate_url
|
||||
|
@ -639,7 +695,7 @@ class URLOption(StringOption):
|
|||
|
||||
|
||||
class FileOption(BaseInputOption):
|
||||
argument_type = "file"
|
||||
type: Literal[OptionType.file] = OptionType.file
|
||||
upload_dirs: List[str] = []
|
||||
|
||||
def __init__(self, question):
|
||||
|
@ -764,12 +820,12 @@ class BaseChoicesOption(BaseInputOption):
|
|||
|
||||
|
||||
class SelectOption(BaseChoicesOption):
|
||||
argument_type = "select"
|
||||
type: Literal[OptionType.select] = OptionType.select
|
||||
default_value = ""
|
||||
|
||||
|
||||
class TagsOption(BaseChoicesOption):
|
||||
argument_type = "tags"
|
||||
type: Literal[OptionType.tags] = OptionType.tags
|
||||
default_value = ""
|
||||
|
||||
@staticmethod
|
||||
|
@ -822,7 +878,7 @@ class TagsOption(BaseChoicesOption):
|
|||
|
||||
|
||||
class DomainOption(BaseChoicesOption):
|
||||
argument_type = "domain"
|
||||
type: Literal[OptionType.domain] = OptionType.domain
|
||||
|
||||
def __init__(self, question):
|
||||
from yunohost.domain import domain_list, _get_maindomain
|
||||
|
@ -851,7 +907,7 @@ class DomainOption(BaseChoicesOption):
|
|||
|
||||
|
||||
class AppOption(BaseChoicesOption):
|
||||
argument_type = "app"
|
||||
type: Literal[OptionType.app] = OptionType.app
|
||||
|
||||
def __init__(self, question):
|
||||
from yunohost.app import app_list
|
||||
|
@ -877,7 +933,7 @@ class AppOption(BaseChoicesOption):
|
|||
|
||||
|
||||
class UserOption(BaseChoicesOption):
|
||||
argument_type = "user"
|
||||
type: Literal[OptionType.user] = OptionType.user
|
||||
|
||||
def __init__(self, question):
|
||||
from yunohost.user import user_list, user_info
|
||||
|
@ -908,7 +964,7 @@ class UserOption(BaseChoicesOption):
|
|||
|
||||
|
||||
class GroupOption(BaseChoicesOption):
|
||||
argument_type = "group"
|
||||
type: Literal[OptionType.group] = OptionType.group
|
||||
|
||||
def __init__(self, question):
|
||||
from yunohost.user import user_group_list
|
||||
|
@ -932,29 +988,29 @@ class GroupOption(BaseChoicesOption):
|
|||
|
||||
|
||||
OPTIONS = {
|
||||
"display_text": DisplayTextOption,
|
||||
"markdown": MarkdownOption,
|
||||
"alert": AlertOption,
|
||||
"button": ButtonOption,
|
||||
"string": StringOption,
|
||||
"text": StringOption,
|
||||
"password": PasswordOption,
|
||||
"color": ColorOption,
|
||||
"number": NumberOption,
|
||||
"range": NumberOption,
|
||||
"boolean": BooleanOption,
|
||||
"date": DateOption,
|
||||
"time": TimeOption,
|
||||
"email": EmailOption,
|
||||
"path": WebPathOption,
|
||||
"url": URLOption,
|
||||
"file": FileOption,
|
||||
"select": SelectOption,
|
||||
"tags": TagsOption,
|
||||
"domain": DomainOption,
|
||||
"app": AppOption,
|
||||
"user": UserOption,
|
||||
"group": GroupOption,
|
||||
OptionType.display_text: DisplayTextOption,
|
||||
OptionType.markdown: MarkdownOption,
|
||||
OptionType.alert: AlertOption,
|
||||
OptionType.button: ButtonOption,
|
||||
OptionType.string: StringOption,
|
||||
OptionType.text: StringOption,
|
||||
OptionType.password: PasswordOption,
|
||||
OptionType.color: ColorOption,
|
||||
OptionType.number: NumberOption,
|
||||
OptionType.range: NumberOption,
|
||||
OptionType.boolean: BooleanOption,
|
||||
OptionType.date: DateOption,
|
||||
OptionType.time: TimeOption,
|
||||
OptionType.email: EmailOption,
|
||||
OptionType.path: WebPathOption,
|
||||
OptionType.url: URLOption,
|
||||
OptionType.file: FileOption,
|
||||
OptionType.select: SelectOption,
|
||||
OptionType.tags: TagsOption,
|
||||
OptionType.domain: DomainOption,
|
||||
OptionType.app: AppOption,
|
||||
OptionType.user: UserOption,
|
||||
OptionType.group: GroupOption,
|
||||
}
|
||||
|
||||
|
||||
|
@ -1122,7 +1178,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", OptionType.string)](raw_question)
|
||||
if isinstance(question, BaseChoicesOption) and question.choices:
|
||||
raw_question["choices"] = question.choices
|
||||
raw_question["default"] = question.default
|
||||
|
|
Loading…
Add table
Reference in a new issue