configpanel: add config panel models

This commit is contained in:
axolotle 2023-04-17 20:08:52 +02:00
parent bec34b92b0
commit 564a66de2f

View file

@ -21,8 +21,10 @@ import os
import re import re
import urllib.parse import urllib.parse
from collections import OrderedDict from collections import OrderedDict
from typing import Union
from logging import getLogger from logging import getLogger
from typing import TYPE_CHECKING, Any, Literal, Sequence, Type, Union
from pydantic import BaseModel, Extra, validator
from moulinette import Moulinette, m18n from moulinette import Moulinette, m18n
from moulinette.interfaces.cli import colorize from moulinette.interfaces.cli import colorize
@ -30,20 +32,206 @@ from moulinette.utils.filesystem import mkdir, read_toml, read_yaml, write_to_ya
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.form import ( from yunohost.utils.form import (
OPTIONS, OPTIONS,
BaseChoicesOption,
BaseInputOption, BaseInputOption,
BaseOption, BaseOption,
BaseReadonlyOption,
FileOption, FileOption,
OptionsModel,
OptionType, OptionType,
Translation,
ask_questions_and_parse_answers, ask_questions_and_parse_answers,
evaluate_simple_js_expression, evaluate_simple_js_expression,
) )
from yunohost.utils.i18n import _value_for_locale from yunohost.utils.i18n import _value_for_locale
if TYPE_CHECKING:
from pydantic.fields import ModelField
logger = getLogger("yunohost.configpanel") logger = getLogger("yunohost.configpanel")
# ╭───────────────────────────────────────────────────────╮
# │ ╭╮╮╭─╮┌─╮┌─╴╷ ╭─╴ │
# │ ││││ ││ │├─╴│ ╰─╮ │
# │ ╵╵╵╰─╯└─╯╰─╴╰─╴╶─╯ │
# ╰───────────────────────────────────────────────────────╯
CONFIG_PANEL_VERSION_SUPPORTED = 1.0 CONFIG_PANEL_VERSION_SUPPORTED = 1.0
class ContainerModel(BaseModel):
id: str
name: Union[Translation, None] = None
services: list[str] = []
help: Union[Translation, None] = None
def translate(self, i18n_key: Union[str, None] = None):
"""
Translate `ask` and `name` attributes of panels and section.
This is in place mutation.
"""
for key in ("help", "name"):
value = getattr(self, key)
if value:
setattr(self, key, _value_for_locale(value))
elif key == "help" and m18n.key_exists(f"{i18n_key}_{self.id}_help"):
setattr(self, key, m18n.n(f"{i18n_key}_{self.id}_help"))
class SectionModel(ContainerModel, OptionsModel):
visible: Union[bool, str] = True
optional: bool = True
# Don't forget to pass arguments to super init
def __init__(
self,
id: str,
name: Union[Translation, None] = None,
services: list[str] = [],
help: Union[Translation, None] = None,
visible: Union[bool, str] = True,
**kwargs,
) -> None:
options = self.options_dict_to_list(kwargs, defaults={"optional": True})
ContainerModel.__init__(
self,
id=id,
name=name,
services=services,
help=help,
visible=visible,
options=options,
)
@property
def is_action_section(self):
return any([option.type is OptionType.button for option in self.options])
def is_visible(self, context: dict[str, Any]):
if isinstance(self.visible, bool):
return self.visible
return evaluate_simple_js_expression(self.visible, context=context)
def translate(self, i18n_key: Union[str, None] = None):
"""
Call to `Container`'s `translate` for self translation
+ Call to `OptionsContainer`'s `translate_options` for options translation
"""
super().translate(i18n_key)
self.translate_options(i18n_key)
class PanelModel(ContainerModel):
# FIXME what to do with `actions?
actions: dict[str, Translation] = {"apply": {"en": "Apply"}}
sections: list[SectionModel]
class Config:
extra = Extra.allow
# Don't forget to pass arguments to super init
def __init__(
self,
id: str,
name: Union[Translation, None] = None,
services: list[str] = [],
help: Union[Translation, None] = None,
**kwargs,
) -> None:
sections = [data | {"id": name} for name, data in kwargs.items()]
super().__init__(
id=id, name=name, services=services, help=help, sections=sections
)
def translate(self, i18n_key: Union[str, None] = None):
"""
Recursivly mutate translatable attributes to their translation
"""
super().translate(i18n_key)
for section in self.sections:
section.translate(i18n_key)
class ConfigPanelModel(BaseModel):
version: float = CONFIG_PANEL_VERSION_SUPPORTED
i18n: Union[str, None] = None
panels: list[PanelModel]
class Config:
arbitrary_types_allowed = True
extra = Extra.allow
# Don't forget to pass arguments to super init
def __init__(
self,
version: float,
i18n: Union[str, None] = None,
**kwargs,
) -> None:
panels = [data | {"id": name} for name, data in kwargs.items()]
super().__init__(version=version, i18n=i18n, panels=panels)
@property
def sections(self):
"""Convinient prop to iter on all sections"""
for panel in self.panels:
for section in panel.sections:
yield section
@property
def options(self):
"""Convinient prop to iter on all options"""
for section in self.sections:
for option in section.options:
yield option
def iter_children(
self,
trigger: list[Literal["panel", "section", "option", "action"]] = ["option"],
):
for panel in self.panels:
if "panel" in trigger:
yield (panel, None, None)
for section in panel.sections:
if "section" in trigger:
yield (panel, section, None)
if "action" in trigger:
for option in section.options:
if option.type is OptionType.button:
yield (panel, section, option)
if "option" in trigger:
for option in section.options:
yield (panel, section, option)
def translate(self):
"""
Recursivly mutate translatable attributes to their translation
"""
for panel in self.panels:
panel.translate(self.i18n)
@validator("version", always=True)
def check_version(cls, value, field: "ModelField"):
if value < CONFIG_PANEL_VERSION_SUPPORTED:
raise ValueError(
f"Config panels version '{value}' are no longer supported."
)
return value
# ╭───────────────────────────────────────────────────────╮
# │ ╭─╴╭─╮╭╮╷┌─╴╶┬╴╭─╮ ╶┬╴╭╮╮┌─╮╷ │
# │ │ │ ││││├─╴ │ │╶╮ │ │││├─╯│ │
# │ ╰─╴╰─╯╵╰╯╵ ╶┴╴╰─╯ ╶┴╴╵╵╵╵ ╰─╴ │
# ╰───────────────────────────────────────────────────────╯
class ConfigPanel: class ConfigPanel:
entity_type = "config" entity_type = "config"
save_path_tpl: Union[str, None] = None save_path_tpl: Union[str, None] = None