mirror of
https://gitflic.ru/project/maks1ms/ocab.git
synced 2025-03-15 06:46:00 +03:00
300 lines
10 KiB
Python
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"),
|
|
)
|