mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
[enh] config panel python method hook
This commit is contained in:
parent
8dbc93c674
commit
9f7fb61b50
3 changed files with 143 additions and 60 deletions
|
@ -1608,14 +1608,9 @@ def app_config_set(
|
||||||
|
|
||||||
|
|
||||||
class AppConfigPanel(ConfigPanel):
|
class AppConfigPanel(ConfigPanel):
|
||||||
def __init__(self, app):
|
entity_type = "app"
|
||||||
|
save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml")
|
||||||
# Check app is installed
|
config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.yml")
|
||||||
_assert_is_installed(app)
|
|
||||||
|
|
||||||
self.app = app
|
|
||||||
config_path = os.path.join(APPS_SETTING_PATH, app, "config_panel.toml")
|
|
||||||
super().__init__(config_path=config_path)
|
|
||||||
|
|
||||||
def _load_current_values(self):
|
def _load_current_values(self):
|
||||||
self.values = self._call_config_script("show")
|
self.values = self._call_config_script("show")
|
||||||
|
@ -1639,7 +1634,7 @@ class AppConfigPanel(ConfigPanel):
|
||||||
from yunohost.hook import hook_exec
|
from yunohost.hook import hook_exec
|
||||||
|
|
||||||
# Add default config script if needed
|
# Add default config script if needed
|
||||||
config_script = os.path.join(APPS_SETTING_PATH, self.app, "scripts", "config")
|
config_script = os.path.join(APPS_SETTING_PATH, self.entity, "scripts", "config")
|
||||||
if not os.path.exists(config_script):
|
if not os.path.exists(config_script):
|
||||||
logger.debug("Adding a default config script")
|
logger.debug("Adding a default config script")
|
||||||
default_script = """#!/bin/bash
|
default_script = """#!/bin/bash
|
||||||
|
@ -1651,15 +1646,15 @@ ynh_app_config_run $1
|
||||||
|
|
||||||
# Call config script to extract current values
|
# Call config script to extract current values
|
||||||
logger.debug(f"Calling '{action}' action from config script")
|
logger.debug(f"Calling '{action}' action from config script")
|
||||||
app_id, app_instance_nb = _parse_app_instance_name(self.app)
|
app_id, app_instance_nb = _parse_app_instance_name(self.entity)
|
||||||
settings = _get_app_settings(app_id)
|
settings = _get_app_settings(app_id)
|
||||||
env.update(
|
env.update(
|
||||||
{
|
{
|
||||||
"app_id": app_id,
|
"app_id": app_id,
|
||||||
"app": self.app,
|
"app": self.entity,
|
||||||
"app_instance_nb": str(app_instance_nb),
|
"app_instance_nb": str(app_instance_nb),
|
||||||
"final_path": settings.get("final_path", ""),
|
"final_path": settings.get("final_path", ""),
|
||||||
"YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, self.app),
|
"YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, self.entity),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -452,14 +452,9 @@ def domain_config_set(
|
||||||
|
|
||||||
|
|
||||||
class DomainConfigPanel(ConfigPanel):
|
class DomainConfigPanel(ConfigPanel):
|
||||||
def __init__(self, domain):
|
entity_type = "domain"
|
||||||
_assert_domain_exists(domain)
|
save_path_tpl = f"{DOMAIN_SETTINGS_PATH}/{entity}.yml")
|
||||||
self.domain = domain
|
save_mode = "diff"
|
||||||
self.save_mode = "diff"
|
|
||||||
super().__init__(
|
|
||||||
config_path=DOMAIN_CONFIG_PATH,
|
|
||||||
save_path=f"{DOMAIN_SETTINGS_DIR}/{domain}.yml",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_toml(self):
|
def _get_toml(self):
|
||||||
from yunohost.dns import _get_registrar_config_section
|
from yunohost.dns import _get_registrar_config_section
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import glob
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
@ -27,7 +28,7 @@ import shutil
|
||||||
import ast
|
import ast
|
||||||
import operator as op
|
import operator as op
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Optional, Dict, List, Union, Any, Mapping
|
from typing import Optional, Dict, List, Union, Any, Mapping, Callable
|
||||||
|
|
||||||
from moulinette.interfaces.cli import colorize
|
from moulinette.interfaces.cli import colorize
|
||||||
from moulinette import Moulinette, m18n
|
from moulinette import Moulinette, m18n
|
||||||
|
@ -189,13 +190,45 @@ def evaluate_simple_js_expression(expr, context={}):
|
||||||
|
|
||||||
|
|
||||||
class ConfigPanel:
|
class ConfigPanel:
|
||||||
def __init__(self, config_path, save_path=None):
|
entity_type = "config"
|
||||||
|
save_path_tpl = None
|
||||||
|
config_path_tpl = "/usr/share/yunohost/other/config_{entity}.toml"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list(cls):
|
||||||
|
"""
|
||||||
|
List available config panel
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
entities = [re.match("^" + cls.save_path_tpl.format(entity="(?p<entity>)") + "$", f).group('entity')
|
||||||
|
for f in glob.glob(cls.save_path_tpl.format(entity="*"))
|
||||||
|
if os.path.isfile(f)]
|
||||||
|
except FileNotFoundError:
|
||||||
|
entities = []
|
||||||
|
return entities
|
||||||
|
|
||||||
|
def __init__(self, entity, config_path=None, save_path=None, creation=False):
|
||||||
|
self.entity = entity
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
|
if not config_path:
|
||||||
|
self.config_path = self.config_path_tpl.format(entity=entity)
|
||||||
self.save_path = save_path
|
self.save_path = save_path
|
||||||
|
if not save_path and self.save_path_tpl:
|
||||||
|
self.save_path = self.save_path_tpl.format(entity=entity)
|
||||||
self.config = {}
|
self.config = {}
|
||||||
self.values = {}
|
self.values = {}
|
||||||
self.new_values = {}
|
self.new_values = {}
|
||||||
|
|
||||||
|
if self.save_path and not creation and not os.path.exists(self.save_path):
|
||||||
|
raise YunohostError(f"{self.entity_type}_doesnt_exists", name=entity)
|
||||||
|
if self.save_path and creation and os.path.exists(self.save_path):
|
||||||
|
raise YunohostError(f"{self.entity_type}_already_exists", name=entity)
|
||||||
|
|
||||||
|
# Search for hooks in the config panel
|
||||||
|
self.hooks = {func: getattr(self, func)
|
||||||
|
for func in dir(self)
|
||||||
|
if callable(getattr(self, func)) and re.match("^(validate|post_ask)__", func)}
|
||||||
|
|
||||||
def get(self, key="", mode="classic"):
|
def get(self, key="", mode="classic"):
|
||||||
self.filter_key = key or ""
|
self.filter_key = key or ""
|
||||||
|
|
||||||
|
@ -274,19 +307,12 @@ class ConfigPanel:
|
||||||
|
|
||||||
# Import and parse pre-answered options
|
# Import and parse pre-answered options
|
||||||
logger.debug("Import and parse pre-answered options")
|
logger.debug("Import and parse pre-answered options")
|
||||||
args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
|
self._parse_pre_answered(args, value, args_file)
|
||||||
self.args = {key: ",".join(value_) for key, value_ in args.items()}
|
|
||||||
|
|
||||||
if args_file:
|
|
||||||
# Import YAML / JSON file but keep --args values
|
|
||||||
self.args = {**read_yaml(args_file), **self.args}
|
|
||||||
|
|
||||||
if value is not None:
|
|
||||||
self.args = {self.filter_key.split(".")[-1]: value}
|
|
||||||
|
|
||||||
# Read or get values and hydrate the config
|
# Read or get values and hydrate the config
|
||||||
self._load_current_values()
|
self._load_current_values()
|
||||||
self._hydrate()
|
self._hydrate()
|
||||||
|
Question.operation_logger = operation_logger
|
||||||
self._ask()
|
self._ask()
|
||||||
|
|
||||||
if operation_logger:
|
if operation_logger:
|
||||||
|
@ -525,7 +551,12 @@ class ConfigPanel:
|
||||||
display_header(f"\n# {name}")
|
display_header(f"\n# {name}")
|
||||||
|
|
||||||
# Check and ask unanswered questions
|
# Check and ask unanswered questions
|
||||||
questions = ask_questions_and_parse_answers(section["options"], self.args)
|
questions = ask_questions_and_parse_answers(
|
||||||
|
section["options"],
|
||||||
|
prefilled_answers=self.args,
|
||||||
|
current_values=self.values,
|
||||||
|
hooks=self.hooks
|
||||||
|
)
|
||||||
self.new_values.update(
|
self.new_values.update(
|
||||||
{
|
{
|
||||||
question.name: question.value
|
question.name: question.value
|
||||||
|
@ -543,20 +574,42 @@ class ConfigPanel:
|
||||||
if "default" in option
|
if "default" in option
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def future_values(self): # TODO put this in ConfigPanel ?
|
||||||
|
return {**self.values, **self.new_values}
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if "new_values" in self.__dict__ and name in self.new_values:
|
||||||
|
return self.new_values[name]
|
||||||
|
|
||||||
|
if "values" in self.__dict__ and name in self.values:
|
||||||
|
return self.values[name]
|
||||||
|
|
||||||
|
return self.__dict__[name]
|
||||||
|
|
||||||
def _load_current_values(self):
|
def _load_current_values(self):
|
||||||
"""
|
"""
|
||||||
Retrieve entries in YAML file
|
Retrieve entries in YAML file
|
||||||
And set default values if needed
|
And set default values if needed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Retrieve entries in the YAML
|
|
||||||
on_disk_settings = {}
|
|
||||||
if os.path.exists(self.save_path) and os.path.isfile(self.save_path):
|
|
||||||
on_disk_settings = read_yaml(self.save_path) or {}
|
|
||||||
|
|
||||||
# Inject defaults if needed (using the magic .update() ;))
|
# Inject defaults if needed (using the magic .update() ;))
|
||||||
self.values = self._get_default_values()
|
self.values = self._get_default_values()
|
||||||
self.values.update(on_disk_settings)
|
|
||||||
|
# Retrieve entries in the YAML
|
||||||
|
if os.path.exists(self.save_path) and os.path.isfile(self.save_path):
|
||||||
|
self.values.update(read_yaml(self.save_path) or {})
|
||||||
|
|
||||||
|
def _parse_pre_answered(self, args, value, args_file):
|
||||||
|
args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
|
||||||
|
self.args = {key: ",".join(value_) for key, value_ in args.items()}
|
||||||
|
|
||||||
|
if args_file:
|
||||||
|
# Import YAML / JSON file but keep --args values
|
||||||
|
self.args = {**read_yaml(args_file), **self.args}
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
self.args = {self.filter_key.split(".")[-1]: value}
|
||||||
|
|
||||||
def _apply(self):
|
def _apply(self):
|
||||||
logger.info("Saving the new configuration...")
|
logger.info("Saving the new configuration...")
|
||||||
|
@ -564,7 +617,7 @@ class ConfigPanel:
|
||||||
if not os.path.exists(dir_path):
|
if not os.path.exists(dir_path):
|
||||||
mkdir(dir_path, mode=0o700)
|
mkdir(dir_path, mode=0o700)
|
||||||
|
|
||||||
values_to_save = {**self.values, **self.new_values}
|
values_to_save = self.future_values
|
||||||
if self.save_mode == "diff":
|
if self.save_mode == "diff":
|
||||||
defaults = self._get_default_values()
|
defaults = self._get_default_values()
|
||||||
values_to_save = {
|
values_to_save = {
|
||||||
|
@ -587,8 +640,8 @@ class ConfigPanel:
|
||||||
if services_to_reload:
|
if services_to_reload:
|
||||||
logger.info("Reloading services...")
|
logger.info("Reloading services...")
|
||||||
for service in services_to_reload:
|
for service in services_to_reload:
|
||||||
if hasattr(self, "app"):
|
if hasattr(self, "entity"):
|
||||||
service = service.replace("__APP__", self.app)
|
service = service.replace("__APP__", self.entity)
|
||||||
service_reload_or_restart(service)
|
service_reload_or_restart(service)
|
||||||
|
|
||||||
def _iterate(self, trigger=["option"]):
|
def _iterate(self, trigger=["option"]):
|
||||||
|
@ -607,13 +660,16 @@ class Question(object):
|
||||||
hide_user_input_in_prompt = False
|
hide_user_input_in_prompt = False
|
||||||
pattern: Optional[Dict] = None
|
pattern: Optional[Dict] = None
|
||||||
|
|
||||||
def __init__(self, question: Dict[str, Any], context: Mapping[str, Any] = {}):
|
def __init__(self, question: Dict[str, Any],
|
||||||
|
context: Mapping[str, Any] = {},
|
||||||
|
hooks: Dict[str, Callable] = {}):
|
||||||
self.name = question["name"]
|
self.name = question["name"]
|
||||||
|
self.context = context
|
||||||
|
self.hooks = hooks
|
||||||
self.type = question.get("type", "string")
|
self.type = question.get("type", "string")
|
||||||
self.default = question.get("default", None)
|
self.default = question.get("default", None)
|
||||||
self.optional = question.get("optional", False)
|
self.optional = question.get("optional", False)
|
||||||
self.visible = question.get("visible", None)
|
self.visible = question.get("visible", None)
|
||||||
self.context = context
|
|
||||||
self.choices = question.get("choices", [])
|
self.choices = question.get("choices", [])
|
||||||
self.pattern = question.get("pattern", self.pattern)
|
self.pattern = question.get("pattern", self.pattern)
|
||||||
self.ask = question.get("ask", {"en": self.name})
|
self.ask = question.get("ask", {"en": self.name})
|
||||||
|
@ -623,6 +679,8 @@ class Question(object):
|
||||||
self.current_value = question.get("current_value")
|
self.current_value = question.get("current_value")
|
||||||
# .value is the "proposed" value which we got from the user
|
# .value is the "proposed" value which we got from the user
|
||||||
self.value = question.get("value")
|
self.value = question.get("value")
|
||||||
|
# Use to return several values in case answer is in mutipart
|
||||||
|
self.values = {}
|
||||||
|
|
||||||
# Empty value is parsed as empty string
|
# Empty value is parsed as empty string
|
||||||
if self.default == "":
|
if self.default == "":
|
||||||
|
@ -663,8 +721,8 @@ class Question(object):
|
||||||
# - we doesn't want to give a specific value
|
# - we doesn't want to give a specific value
|
||||||
# - we want to keep the previous value
|
# - we want to keep the previous value
|
||||||
# - we want the default value
|
# - we want the default value
|
||||||
self.value = None
|
self.value = self.values[self.name] = None
|
||||||
return self.value
|
return self.values
|
||||||
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
# Display question if no value filled or if it's a readonly message
|
# Display question if no value filled or if it's a readonly message
|
||||||
|
@ -698,9 +756,18 @@ class Question(object):
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
self.value = self._post_parse_value()
|
self.value = self.values[self.name] = self._post_parse_value()
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
return self.value
|
return self.value
|
||||||
|
=======
|
||||||
|
# Search for post actions in hooks
|
||||||
|
post_hook = f"post_ask__{self.name}"
|
||||||
|
if post_hook in self.hooks:
|
||||||
|
self.values.update(self.hooks[post_hook](self))
|
||||||
|
|
||||||
|
return self.values
|
||||||
|
>>>>>>> c0cd8dbf... [enh] config panel python method hook
|
||||||
|
|
||||||
def _prevalidate(self):
|
def _prevalidate(self):
|
||||||
if self.value in [None, ""] and not self.optional:
|
if self.value in [None, ""] and not self.optional:
|
||||||
|
@ -864,8 +931,10 @@ class PasswordQuestion(Question):
|
||||||
default_value = ""
|
default_value = ""
|
||||||
forbidden_chars = "{}"
|
forbidden_chars = "{}"
|
||||||
|
|
||||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
def __init__(self, question,
|
||||||
super().__init__(question, context)
|
context: Mapping[str, Any] = {},
|
||||||
|
hooks: Dict[str, Callable] = {}):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
self.redact = True
|
self.redact = True
|
||||||
if self.default is not None:
|
if self.default is not None:
|
||||||
raise YunohostValidationError(
|
raise YunohostValidationError(
|
||||||
|
@ -983,8 +1052,9 @@ class BooleanQuestion(Question):
|
||||||
choices="yes/no",
|
choices="yes/no",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||||
super().__init__(question, context)
|
hooks: Dict[str, Callable] = {}):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
self.yes = question.get("yes", 1)
|
self.yes = question.get("yes", 1)
|
||||||
self.no = question.get("no", 0)
|
self.no = question.get("no", 0)
|
||||||
if self.default is None:
|
if self.default is None:
|
||||||
|
@ -1004,10 +1074,11 @@ class BooleanQuestion(Question):
|
||||||
class DomainQuestion(Question):
|
class DomainQuestion(Question):
|
||||||
argument_type = "domain"
|
argument_type = "domain"
|
||||||
|
|
||||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||||
|
hooks: Dict[str, Callable] = {}):
|
||||||
from yunohost.domain import domain_list, _get_maindomain
|
from yunohost.domain import domain_list, _get_maindomain
|
||||||
|
|
||||||
super().__init__(question, context)
|
super().__init__(question, context, hooks)
|
||||||
|
|
||||||
if self.default is None:
|
if self.default is None:
|
||||||
self.default = _get_maindomain()
|
self.default = _get_maindomain()
|
||||||
|
@ -1030,11 +1101,12 @@ class DomainQuestion(Question):
|
||||||
class UserQuestion(Question):
|
class UserQuestion(Question):
|
||||||
argument_type = "user"
|
argument_type = "user"
|
||||||
|
|
||||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||||
|
hooks: Dict[str, Callable] = {}):
|
||||||
from yunohost.user import user_list, user_info
|
from yunohost.user import user_list, user_info
|
||||||
from yunohost.domain import _get_maindomain
|
from yunohost.domain import _get_maindomain
|
||||||
|
|
||||||
super().__init__(question, context)
|
super().__init__(question, context, hooks)
|
||||||
self.choices = list(user_list()["users"].keys())
|
self.choices = list(user_list()["users"].keys())
|
||||||
|
|
||||||
if not self.choices:
|
if not self.choices:
|
||||||
|
@ -1056,8 +1128,9 @@ class NumberQuestion(Question):
|
||||||
argument_type = "number"
|
argument_type = "number"
|
||||||
default_value = None
|
default_value = None
|
||||||
|
|
||||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||||
super().__init__(question, context)
|
hooks: Dict[str, Callable] = {}):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
self.min = question.get("min", None)
|
self.min = question.get("min", None)
|
||||||
self.max = question.get("max", None)
|
self.max = question.get("max", None)
|
||||||
self.step = question.get("step", None)
|
self.step = question.get("step", None)
|
||||||
|
@ -1108,8 +1181,9 @@ class DisplayTextQuestion(Question):
|
||||||
argument_type = "display_text"
|
argument_type = "display_text"
|
||||||
readonly = True
|
readonly = True
|
||||||
|
|
||||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||||
super().__init__(question, context)
|
hooks: Dict[str, Callable] = {}):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
|
|
||||||
self.optional = True
|
self.optional = True
|
||||||
self.style = question.get(
|
self.style = question.get(
|
||||||
|
@ -1143,8 +1217,9 @@ class FileQuestion(Question):
|
||||||
if os.path.exists(upload_dir):
|
if os.path.exists(upload_dir):
|
||||||
shutil.rmtree(upload_dir)
|
shutil.rmtree(upload_dir)
|
||||||
|
|
||||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||||
super().__init__(question, context)
|
hooks: Dict[str, Callable] = {}):
|
||||||
|
super().__init__(question, context, hooks)
|
||||||
self.accept = question.get("accept", "")
|
self.accept = question.get("accept", "")
|
||||||
|
|
||||||
def _prevalidate(self):
|
def _prevalidate(self):
|
||||||
|
@ -1214,7 +1289,14 @@ ARGUMENTS_TYPE_PARSERS = {
|
||||||
|
|
||||||
|
|
||||||
def ask_questions_and_parse_answers(
|
def ask_questions_and_parse_answers(
|
||||||
|
<<<<<<< HEAD
|
||||||
raw_questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}
|
raw_questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}
|
||||||
|
=======
|
||||||
|
raw_questions: Dict,
|
||||||
|
prefilled_answers: Union[str, Mapping[str, Any]] = {},
|
||||||
|
current_values: Union[str, Mapping[str, Any]] = {},
|
||||||
|
hooks: Dict[str, Callable[[], None]] = {}
|
||||||
|
>>>>>>> c0cd8dbf... [enh] config panel python method hook
|
||||||
) -> List[Question]:
|
) -> List[Question]:
|
||||||
"""Parse arguments store in either manifest.json or actions.json or from a
|
"""Parse arguments store in either manifest.json or actions.json or from a
|
||||||
config panel against the user answers when they are present.
|
config panel against the user answers when they are present.
|
||||||
|
@ -1241,13 +1323,24 @@ def ask_questions_and_parse_answers(
|
||||||
else:
|
else:
|
||||||
answers = {}
|
answers = {}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
context = {**current_values, **answers}
|
||||||
|
>>>>>>> c0cd8dbf... [enh] config panel python method hook
|
||||||
out = []
|
out = []
|
||||||
|
|
||||||
for raw_question in raw_questions:
|
for raw_question in raw_questions:
|
||||||
question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]
|
question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]
|
||||||
raw_question["value"] = answers.get(raw_question["name"])
|
raw_question["value"] = answers.get(raw_question["name"])
|
||||||
|
<<<<<<< HEAD
|
||||||
question = question_class(raw_question, context=answers)
|
question = question_class(raw_question, context=answers)
|
||||||
answers[question.name] = question.ask_if_needed()
|
answers[question.name] = question.ask_if_needed()
|
||||||
|
=======
|
||||||
|
question = question_class(raw_question, context=context, hooks=hooks)
|
||||||
|
new_values = question.ask_if_needed()
|
||||||
|
answers.update(new_values)
|
||||||
|
context.update(new_values)
|
||||||
|
>>>>>>> c0cd8dbf... [enh] config panel python method hook
|
||||||
out.append(question)
|
out.append(question)
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
Loading…
Add table
Reference in a new issue