"""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.check_condition_fallback() self.convert_condition_source() self.convert_param(self.objectspace.space.constraints.condition) self.check_source_target() self.convert_xxxlist() self.check_choice_option_condition() self.remove_condition_with_empty_target() self.convert_condition() def valid_type_validation(self, obj, ) -> None: if obj.source.type == 'choice': return obj.source.ori_type return obj.source.type 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.path == target.name.path: msg = _('target name and source name must be different: ' f'{condition.source.path}') 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.path fill.param.append(param3) param4 = self.objectspace.param(target.xmlfiles) param4.name = f'expected_{fill.index}' param4.text = values param4.type = condition.param[0].type 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 """ for condition in self.objectspace.space.constraints.condition: actions = self.get_actions_from_condition(condition.name) for param in condition.param: 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, param, 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, param, condition, action, ) def build_property(self, obj, param: 'self.objectspace.param', 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 = param prop.name = action if not hasattr(obj, 'properties'): obj.properties = [] obj.properties.append(prop)