0
0
mirror of https://gitflic.ru/project/maks1ms/ocab.git synced 2025-03-15 06:46:00 +03:00
ocab/src/ocab_modules/standard/config/config_manager.py
2024-07-22 00:44:27 +03:00

300 lines
10 KiB
Python

from typing import Any, Dict, List, Optional, Union
import flask
import yaml
try:
import dash_bootstrap_components as dbc
from dash_extensions.enrich import ALL, Input, Output, State, dcc, html
DASH_AVAILABLE = True
except ImportError:
DASH_AVAILABLE = False
class ConfigManager:
def __init__(self, config_path: str):
self._config_path = config_path
self._config = self.load_config()
self._registered_settings = dict()
self._registered_settings_meta = dict()
self._update_callbacks = []
self._update_required = False
@property
def config(self):
return self._config
@config.setter
def config(self, value):
self._config = value
def load_config(self) -> Dict[str, Any]:
with open(self._config_path, "r") as file:
return yaml.safe_load(file)
def save_config(self):
with open(self._config_path, "w") as file:
yaml.dump(self._config, file, allow_unicode=True)
def register_setting(
self,
key: Union[str, List[str]],
default_value: Any,
setting_type: str,
is_private: bool = False,
pretty_name: str = None,
description: str = None,
options: Optional[List[str]] = None,
):
if isinstance(key, str):
key = [key]
current = self._registered_settings
for k in key[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[key[-1]] = self.get_nested_setting(self._config, key, default_value)
self.set_nested_setting(
self._registered_settings_meta,
key,
{
"type": setting_type,
"is_private": is_private,
"options": options,
"pretty_name": pretty_name,
"description": description,
},
)
def get_nested_setting(
self, config: dict, keys: List[str], default: Any = None
) -> Any:
current = config
for key in keys:
if key in current:
current = current[key]
else:
return default
return current
def set_nested_setting(self, config: dict, keys: List[str], value: Any):
current = config
for key in keys[:-1]:
if key not in current:
current[key] = {}
current = current[key]
current[keys[-1]] = value
def __getitem__(self, key):
if key in self._registered_settings:
return self._registered_settings[key]
raise KeyError(key)
def update_setting(self, key: Union[str, List[str]], value: Any):
if isinstance(key, str):
key = [key]
self.set_nested_setting(self._registered_settings, key, value)
self.set_nested_setting(self._config, key, value)
self.save_config()
def get_settings_layout(self, prefix):
from dash_extensions.enrich import DashBlueprint
self._prefix = prefix
bp = DashBlueprint()
def create_layout():
def create_nested_layout(settings: dict, key_list=None):
if key_list is None:
key_list = []
components = []
for key, value in settings.items():
current_key_list = key_list.copy()
current_key_list.append(key)
if isinstance(value, dict):
nested = create_nested_layout(value, current_key_list)
if len(nested) > 0:
components.append(
dbc.Card(
[
dbc.CardHeader(html.H3(key, className="mb-0")),
dbc.CardBody(nested),
],
className="mb-3",
)
)
else:
meta = self.get_nested_setting(
self._registered_settings_meta, current_key_list
)
if not meta.get("is_private"):
row = []
label_text = meta.get("pretty_name", key)
if not label_text:
label_text = key
if meta.get("type") != "checkbox":
row.append(dbc.Label(label_text))
component_id = {
"type": "setting",
"key": "::".join(current_key_list),
}
if meta.get("type") == "string":
component = dbc.Input(
id=component_id,
type="text",
value=value,
)
elif meta.get("type") == "number":
component = dbc.Input(
id=component_id,
type="number",
value=value,
)
elif meta.get("type") == "checkbox":
component = dbc.Col(
[
dbc.Checkbox(
id=component_id,
value=value,
label=label_text,
)
]
)
elif meta.get("type") == "select":
options = [
{"label": opt, "value": opt}
for opt in meta.get("options", [])
]
component = dcc.Dropdown(
id=component_id,
options=options,
value=value,
)
else:
continue
row.append(component)
if meta.get("description"):
row.append(dbc.FormText(meta.get("description")))
components.append(dbc.Row(row, className="mb-3"))
return components
settings_components = create_nested_layout(self._registered_settings)
layout = html.Div(
[
html.H1("Настройки"),
dbc.Form(settings_components),
html.Div(id="save-confirmation"),
dbc.Button(
"Сохранить",
id="save-settings",
color="primary",
className="mt-3 w-100",
n_clicks=0,
),
html.Div(id="settings-update-trigger", style={"display": "none"}),
dcc.Store(id="settings-store"),
],
style={
"padding": "20px",
},
)
return layout
bp.layout = create_layout
self.setup_callbacks(bp)
return bp
def setup_callbacks(self, app):
@app.callback(
Output("save-confirmation", "children", allow_duplicate=True),
Output("settings-store", "data"),
Input("save-settings", "n_clicks"),
State({"type": "setting", "key": ALL}, "value"),
State({"type": "setting", "key": ALL}, "id"),
prevent_initial_call=True,
allow_duplicate=True,
# https://github.com/emilhe/dash-extensions/issues/344
# running=[
# (
# Output({
# id: "save-settings",
# 'n_clicks': MATCH
# }, "disabled"), True, False
# )
# ]
)
def save_settings(n_clicks, values, keys):
print(flask.g.user)
if n_clicks > 0:
updated_settings = {}
for value, id_dict in zip(values, keys):
key: str = id_dict["key"]
if self._prefix:
key = key.removeprefix(f"{self._prefix}-")
self.update_setting(key.split("::"), value)
updated_settings[key] = value
import datetime
import locale
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
now = datetime.datetime.now()
date_str = now.strftime("%H:%M:%S")
return (
dbc.Alert(
f"Настройки сохранены в {date_str}",
color="success",
duration=10000,
),
date_str,
)
app.clientside_callback(
"""
function(n_clicks) {
const buttonSelector = '#%s-save-settings';
if (n_clicks > 0) {
document.querySelector(buttonSelector).disabled = true;
}
}
"""
% (self._prefix),
Input("save-settings", "n_clicks"),
)
app.clientside_callback(
"""
function(data) {
const buttonSelector = '#%s-save-settings';
if (data) {
document.querySelector(buttonSelector).disabled = false;
}
}
"""
% (self._prefix),
Input("settings-store", "data"),
)