361 lines
16 KiB
Python
361 lines
16 KiB
Python
"""Annotate condition
|
|
|
|
Created by:
|
|
EOLE (http://eole.orion.education.fr)
|
|
Copyright (C) 2005-2018
|
|
|
|
Forked by:
|
|
Cadoles (http://www.cadoles.com)
|
|
Copyright (C) 2019-2021
|
|
|
|
distribued with GPL-2 or later license
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
"""
|
|
from typing import List, Any
|
|
|
|
|
|
from ..i18n import _
|
|
from ..error import DictConsistencyError
|
|
from ..config import Config
|
|
|
|
from .target import TargetAnnotator
|
|
from .param import ParamAnnotator
|
|
from .variable import Walk
|
|
|
|
FREEZE_AUTOFREEZE_VARIABLE = 'module_instancie'
|
|
|
|
|
|
class ConditionAnnotator(TargetAnnotator, ParamAnnotator, Walk):
|
|
"""Annotate condition
|
|
"""
|
|
def __init__(self,
|
|
objectspace,
|
|
):
|
|
self.objectspace = objectspace
|
|
self.force_service_value = {}
|
|
if hasattr(objectspace.space, 'variables'):
|
|
self.convert_auto_freeze()
|
|
if not hasattr(objectspace.space, 'constraints') or \
|
|
not hasattr(self.objectspace.space.constraints, 'condition'):
|
|
return
|
|
self.target_is_uniq = False
|
|
self.only_variable = False
|
|
self.convert_target(self.objectspace.space.constraints.condition)
|
|
self.convert_param(self.objectspace.space.constraints.condition)
|
|
self.check_source_target()
|
|
self.check_condition_fallback()
|
|
self.convert_xxxlist()
|
|
self.convert_condition_source()
|
|
self.check_choice_option_condition()
|
|
self.remove_condition_with_empty_target()
|
|
self.convert_condition()
|
|
|
|
def convert_auto_freeze(self):
|
|
"""convert auto_freeze
|
|
only if FREEZE_AUTOFREEZE_VARIABLE == 'oui' this variable is frozen
|
|
"""
|
|
for variable in self.get_variables():
|
|
self._convert_auto_freeze(variable)
|
|
|
|
def _convert_auto_freeze(self,
|
|
variable: 'self.objectspace.variable',
|
|
) -> None:
|
|
if not variable.auto_freeze:
|
|
return
|
|
if variable.namespace != Config['variable_namespace']:
|
|
msg = _(f'auto_freeze is not allowed in extra "{variable.namespace}"')
|
|
raise DictConsistencyError(msg, 49, variable.xmlfiles)
|
|
new_condition = self.objectspace.condition(variable.xmlfiles)
|
|
new_condition.name = 'auto_frozen_if_not_in'
|
|
new_condition.namespace = variable.namespace
|
|
new_condition.source = FREEZE_AUTOFREEZE_VARIABLE
|
|
new_param = self.objectspace.param(variable.xmlfiles)
|
|
new_param.text = 'oui'
|
|
new_condition.param = [new_param]
|
|
new_target = self.objectspace.target(variable.xmlfiles)
|
|
new_target.type = 'variable'
|
|
new_target.name = variable.name
|
|
new_condition.target = [new_target]
|
|
if not hasattr(self.objectspace.space, 'constraints'):
|
|
self.objectspace.space.constraints = self.objectspace.constraints(variable.xmlfiles)
|
|
if not hasattr(self.objectspace.space.constraints, 'condition'):
|
|
self.objectspace.space.constraints.condition = []
|
|
self.objectspace.space.constraints.condition.append(new_condition)
|
|
|
|
def check_source_target(self):
|
|
"""verify that source != target in condition
|
|
"""
|
|
for condition in self.objectspace.space.constraints.condition:
|
|
for target in condition.target:
|
|
if target.type == 'variable' and \
|
|
condition.source in [target.name.name, target.name.path]:
|
|
msg = _('target name and source name must be different: '
|
|
f'{condition.source}')
|
|
raise DictConsistencyError(msg, 11, condition.xmlfiles)
|
|
|
|
def check_condition_fallback(self):
|
|
"""a condition with a fallback **and** the source variable doesn't exist
|
|
"""
|
|
remove_conditions = []
|
|
for idx, condition in enumerate(self.objectspace.space.constraints.condition):
|
|
# fallback
|
|
if condition.fallback is False or \
|
|
self.objectspace.paths.path_is_defined(condition.source):
|
|
continue
|
|
if condition.name in ['disabled_if_in', 'mandatory_if_in', 'hidden_if_in']:
|
|
apply_action = not condition.force_condition_on_fallback
|
|
else:
|
|
apply_action = condition.force_inverse_condition_on_fallback
|
|
remove_conditions.append(idx)
|
|
if apply_action:
|
|
self.force_actions_to_variable(condition)
|
|
remove_conditions.sort(reverse=True)
|
|
for idx in remove_conditions:
|
|
self.objectspace.space.constraints.condition.pop(idx)
|
|
|
|
def force_actions_to_variable(self,
|
|
condition: 'self.objectspace.condition',
|
|
) -> None:
|
|
"""force property to a variable
|
|
for example disabled_if_not_in => variable.disabled = True
|
|
"""
|
|
actions = self.get_actions_from_condition(condition.name)
|
|
for target in condition.target:
|
|
main_action = actions[0]
|
|
if target.type.endswith('list'):
|
|
self.force_service_value[target.name] = main_action != 'disabled'
|
|
continue
|
|
leader_or_var, variables = self._get_family_variables_from_target(target)
|
|
setattr(leader_or_var, main_action, True)
|
|
for action in actions[1:]:
|
|
for variable in variables:
|
|
setattr(variable, action, True)
|
|
|
|
@staticmethod
|
|
def get_actions_from_condition(condition_name: str) -> List[str]:
|
|
"""get action's name from a condition
|
|
"""
|
|
if condition_name.startswith('hidden_if_'):
|
|
return ['hidden', 'frozen', 'force_default_on_freeze']
|
|
if condition_name == 'auto_frozen_if_not_in':
|
|
return ['auto_frozen']
|
|
return [condition_name.split('_', 1)[0]]
|
|
|
|
def _get_family_variables_from_target(self,
|
|
target,
|
|
):
|
|
if target.type == 'variable':
|
|
if not self.objectspace.paths.is_leader(target.name.path):
|
|
return target.name, [target.name]
|
|
# it's a leader, so apply property to leadership
|
|
family_name = self.objectspace.paths.get_variable_family_path(target.name.path)
|
|
family = self.objectspace.paths.get_family(family_name,
|
|
target.name.namespace,
|
|
)
|
|
return family, family.variable
|
|
# it's a family
|
|
variable = self.objectspace.paths.get_family(target.name.path,
|
|
target.namespace,
|
|
)
|
|
if hasattr(variable, 'variable'):
|
|
return variable, list(variable.variable.values())
|
|
return variable, []
|
|
|
|
def convert_xxxlist(self):
|
|
"""transform *list to variable or family
|
|
"""
|
|
fills = {}
|
|
for condition in self.objectspace.space.constraints.condition:
|
|
remove_targets = []
|
|
for target_idx, target in enumerate(condition.target):
|
|
if target.type.endswith('list'):
|
|
listvars = self.objectspace.list_conditions.get(target.type,
|
|
{}).get(target.name)
|
|
if listvars:
|
|
self._convert_xxxlist_to_fill(condition,
|
|
target,
|
|
listvars,
|
|
fills,
|
|
)
|
|
remove_targets.append(target_idx)
|
|
remove_targets.sort(reverse=True)
|
|
for target_idx in remove_targets:
|
|
condition.target.pop(target_idx)
|
|
|
|
def _convert_xxxlist_to_fill(self,
|
|
condition: 'self.objectspace.condition',
|
|
target: 'self.objectspace.target',
|
|
listvars: list,
|
|
fills: dict,
|
|
):
|
|
for listvar in listvars:
|
|
if target.name in self.force_service_value:
|
|
listvar.default = self.force_service_value[target.name]
|
|
continue
|
|
if listvar.path in fills:
|
|
fill = fills[listvar.path]
|
|
or_needed = True
|
|
for param in fill.param:
|
|
if hasattr(param, 'name') and \
|
|
param.name == 'condition_operator':
|
|
or_needed = False
|
|
break
|
|
fill.index += 1
|
|
else:
|
|
fill = self.objectspace.fill(target.xmlfiles)
|
|
new_target = self.objectspace.target(target.xmlfiles)
|
|
new_target.name = listvar.path
|
|
fill.target = [new_target]
|
|
fill.name = 'calc_value'
|
|
fill.namespace = 'services'
|
|
fill.index = 0
|
|
if not hasattr(self.objectspace.space.constraints, 'fill'):
|
|
self.objectspace.space.constraints.fill = []
|
|
self.objectspace.space.constraints.fill.append(fill)
|
|
fills[listvar.path] = fill
|
|
param1 = self.objectspace.param(target.xmlfiles)
|
|
param1.text = False
|
|
param1.type = 'boolean'
|
|
param2 = self.objectspace.param(target.xmlfiles)
|
|
param2.name = 'default'
|
|
param2.text = True
|
|
param2.type = 'boolean'
|
|
fill.param = [param1, param2]
|
|
or_needed = len(condition.param) != 1
|
|
if len(condition.param) == 1:
|
|
values = getattr(condition.param[0], 'text', None)
|
|
else:
|
|
values = tuple([getattr(param, 'text', None) for param in condition.param])
|
|
param3 = self.objectspace.param(target.xmlfiles)
|
|
param3.name = f'condition_{fill.index}'
|
|
param3.type = 'variable'
|
|
param3.text = condition.source
|
|
fill.param.append(param3)
|
|
param4 = self.objectspace.param(target.xmlfiles)
|
|
param4.name = f'expected_{fill.index}'
|
|
param4.text = values
|
|
fill.param.append(param4)
|
|
if condition.name != 'disabled_if_in':
|
|
param5 = self.objectspace.param(target.xmlfiles)
|
|
param5.name = f'reverse_condition_{fill.index}'
|
|
param5.text = True
|
|
param5.type = 'boolean'
|
|
fill.param.append(param5)
|
|
if or_needed:
|
|
param6 = self.objectspace.param(target.xmlfiles)
|
|
param6.name = 'condition_operator'
|
|
param6.text = 'OR'
|
|
fill.param.append(param6)
|
|
|
|
def convert_condition_source(self):
|
|
"""remove condition for ChoiceOption that don't have param
|
|
"""
|
|
for condition in self.objectspace.space.constraints.condition:
|
|
try:
|
|
condition.source = self.objectspace.paths.get_variable(condition.source)
|
|
except DictConsistencyError as err:
|
|
if err.errno == 36:
|
|
msg = _(f'the source "{condition.source}" in condition cannot be a dynamic '
|
|
f'variable')
|
|
raise DictConsistencyError(msg, 20, condition.xmlfiles) from err
|
|
if err.errno == 42:
|
|
msg = _(f'the source "{condition.source}" in condition is an unknown variable')
|
|
raise DictConsistencyError(msg, 23, condition.xmlfiles) from err
|
|
raise err from err # pragma: no cover
|
|
|
|
def check_choice_option_condition(self):
|
|
"""remove condition of ChoiceOption that doesn't match
|
|
"""
|
|
remove_conditions = []
|
|
for condition_idx, condition in enumerate(self.objectspace.space.constraints.condition):
|
|
if condition.source.path in self.objectspace.valid_enums:
|
|
valid_enum = self.objectspace.valid_enums[condition.source.path]['values']
|
|
remove_param = [param_idx for param_idx, param in enumerate(condition.param) \
|
|
if param.type != 'variable' and param.text not in valid_enum]
|
|
if not remove_param:
|
|
continue
|
|
remove_param.sort(reverse=True)
|
|
for idx in remove_param:
|
|
del condition.param[idx]
|
|
if not condition.param and condition.name.endswith('_if_not_in'):
|
|
self.force_actions_to_variable(condition)
|
|
remove_conditions.append(condition_idx)
|
|
remove_conditions.sort(reverse=True)
|
|
for idx in remove_conditions:
|
|
self.objectspace.space.constraints.condition.pop(idx)
|
|
|
|
def remove_condition_with_empty_target(self):
|
|
"""remove condition with empty target
|
|
"""
|
|
# optional target are remove, condition could be empty
|
|
remove_conditions = [condition_idx for condition_idx, condition in \
|
|
enumerate(self.objectspace.space.constraints.condition) \
|
|
if not condition.target]
|
|
remove_conditions.sort(reverse=True)
|
|
for idx in remove_conditions:
|
|
self.objectspace.space.constraints.condition.pop(idx)
|
|
|
|
def convert_condition(self):
|
|
"""valid and manage <condition>
|
|
"""
|
|
for condition in self.objectspace.space.constraints.condition:
|
|
actions = self.get_actions_from_condition(condition.name)
|
|
for param in condition.param:
|
|
text = getattr(param, 'text', None)
|
|
for target in condition.target:
|
|
leader_or_variable, variables = self._get_family_variables_from_target(target)
|
|
# if option is already disable, do not apply disable_if_in
|
|
# check only the first action (example of multiple actions:
|
|
# 'hidden', 'frozen', 'force_default_on_freeze')
|
|
main_action = actions[0]
|
|
if getattr(leader_or_variable, main_action, False) is True:
|
|
continue
|
|
self.build_property(leader_or_variable,
|
|
text,
|
|
condition,
|
|
main_action,
|
|
)
|
|
if isinstance(leader_or_variable, self.objectspace.variable) and \
|
|
(leader_or_variable.auto_save or leader_or_variable.auto_freeze) and \
|
|
'force_default_on_freeze' in actions:
|
|
continue
|
|
for action in actions[1:]:
|
|
# other actions are set to the variable or children of family
|
|
for variable in variables:
|
|
self.build_property(variable,
|
|
text,
|
|
condition,
|
|
action,
|
|
)
|
|
|
|
def build_property(self,
|
|
obj,
|
|
text: Any,
|
|
condition: 'self.objectspace.condition',
|
|
action: str,
|
|
) -> 'self.objectspace.property_':
|
|
"""build property_ for a condition
|
|
"""
|
|
prop = self.objectspace.property_(obj.xmlfiles)
|
|
prop.type = 'calculation'
|
|
prop.inverse = condition.name.endswith('_if_not_in')
|
|
prop.source = condition.source
|
|
prop.expected = text
|
|
prop.name = action
|
|
if not hasattr(obj, 'properties'):
|
|
obj.properties = []
|
|
obj.properties.append(prop)
|