From 4e797674ef93eba6ebcfdcdf0ed8677165e97ab5 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Thu, 25 Jul 2019 15:22:00 +0200 Subject: [PATCH] Better leadership support --- tiramisu_json_api/api.py | 335 ++++++++++++++++++++++++++------------- 1 file changed, 226 insertions(+), 109 deletions(-) diff --git a/tiramisu_json_api/api.py b/tiramisu_json_api/api.py index 3f7a6af..df5fd27 100644 --- a/tiramisu_json_api/api.py +++ b/tiramisu_json_api/api.py @@ -339,6 +339,16 @@ class TiramisuOptionValue(_Value): self._validate(type_, val) else: self._validate(type_, value) + if self.path in self.temp: + obj = None + if self.index is None: + obj = self.temp[self.path] + elif str(self.index) in self.temp[self.path]: + obj = self.temp[self.path][str(self.index)] + if obj is not None: + for attr in ['error', 'warnings', 'invalid', 'hasWarnings']: + if attr in obj: + del obj[attr] self.config.modify_value(self.path, self.index, value, @@ -369,6 +379,62 @@ class TiramisuOptionValue(_Value): value = self.schema.get('value') return value + def valid(self): + temp = self.config.temp.get(self.path, {}) + model = self.config.model.get(self.path, {}) + if self.index is None: + if 'invalid' in temp: + return not temp['invalid'] + return not model.get('invalid', False) + elif str(self.index) in temp and 'invalid' in temp[str(self.index)]: + return not temp[str(self.index)]['invalid'] + elif str(self.index) in model: + return not model[str(self.index)].get('invalid', False) + return True + + def warning(self): + temp = self.config.temp.get(self.path, {}) + model = self.config.model.get(self.path, {}) + if self.index is None: + if 'hasWarnings' in temp: + return temp['hasWarnings'] + return model.get('hasWarnings', False) + elif str(self.index) in temp and 'hasWarnings' in temp[str(self.index)]: + return temp[str(self.index)]['hasWarnings'] + elif str(self.index) in model: + return model[str(self.index)].get('hasWarnings', False) + return False + + def error_message(self): + temp = self.config.temp.get(self.path, {}) + model = self.config.model.get(self.path, {}) + if self.valid(): + return [] + if self.index is None: + if temp.get('invalid') == True: + return temp['error'] + return model['error'] + elif str(self.index) in temp and 'invalid' in temp[str(self.index)]: + return temp[str(self.index)]['error'] + else: + return model[str(self.index)]['error'] + return [] + + def warning_message(self): + temp = self.config.temp.get(self.path, {}) + model = self.config.model.get(self.path, {}) + if not self.warning(): + return [] + if self.index is None: + if temp.get('hasWarnings') == True: + return temp['warnings'] + return model['warnings'] + elif str(self.index) in temp and 'hasWarnings' in temp[str(self.index)]: + return temp[str(self.index)]['warnings'] + else: + return model[str(self.index)]['warnings'] + return [] + class _Option: def list(self, @@ -376,28 +442,33 @@ class _Option: recursive=False): if type not in ['all', 'option', 'optiondescription']: raise Exception('unknown list type {}'.format(type)) - for path, schema in self.schema['properties'].items(): + for idx_path, path in enumerate(self.schema['properties']): + subschema = self.schema['properties'][path] if not self.config.is_hidden(path, None): - if schema['type'] in ['object', 'array']: + if subschema['type'] in ['object', 'array']: if type in ['all', 'optiondescription']: yield TiramisuOptionDescription(self.config, - schema, + subschema, self.form, self.temp, path) if recursive: yield from TiramisuOptionDescription(self.config, - schema, + subschema, self.form, self.temp, path).list(type, recursive) elif type in ['all', 'option']: yield TiramisuOption(self.config, - schema, + subschema, self.form, self.temp, path, self.index) + elif self.schema.get('type') == 'array' and idx_path == 0: + # if a leader is hidden, follower are hidden too + break + class TiramisuOptionDescription(_Option): @@ -680,15 +751,10 @@ class Config: remote: bool, leader_old_value: Any) -> None: schema = self.get_schema(path) - if value and isinstance(value, list) and undefined in value: - new_value = schema.get('defaultmulti') - if remote: - for idx, val in enumerate(value): - self.manage_updates('modify', - path, - idx, - val) - else: + has_undefined = value is not None and isinstance(value, list) and undefined in value + new_value = schema.get('defaultmulti') + if not remote: + if has_undefined: while undefined in value: undefined_index = value.index(undefined) schema_value = schema.get('value', []) @@ -696,26 +762,25 @@ class Config: value[undefined_index] = schema_value[undefined_index] else: value[undefined_index] = new_value - self.updates_value('modify', - path, - index, - value, - remote, - False, - leader_old_value) + self.updates_value('modify', + path, + index, + value, + remote, + False, + leader_old_value) + self.manage_updates('modify', + path, + index, + value) + + elif has_undefined: + for idx, val in enumerate(value): self.manage_updates('modify', path, - index, - value) + idx, + val) else: - if not remote: - self.updates_value('modify', - path, - index, - value, - remote, - False, - leader_old_value) self.manage_updates('modify', path, index, @@ -980,8 +1045,8 @@ class Config: if DEBUG: print('<===== send') print(self.updates) - self.updates_data(self.send_data({'updates': self.updates, - 'model': self.model})) + self.send_data({'updates': self.updates, + 'model': self.model}) def updates_value(self, action: str, @@ -991,11 +1056,14 @@ class Config: remote: bool, default_value: bool=False, leader_old_value: Optional[Any]=undefined) -> None: - if 'pattern' in self.form.get(path, {}) and (not isinstance(value, list) or undefined not in value) and not self.test_value(path, index, value, remote): - return + # if 'pattern' in self.form.get(path, {}) and (not isinstance(value, list) or undefined not in value) and not self.test_value(path, index, value, remote): + # return if remote: self.send() else: + changes = [] + if self.test_value(path, index, value) and not self.is_hidden(path, index): + changes.append(path) if path in self.model and (index is None or str(index) in self.model[path]): model = self.model[path] if index is not None: @@ -1067,13 +1135,14 @@ class Config: else: self.set_not_equal(path, value, index) # set a value for a follower option - self.temp.setdefault(path, {})[str(index)] = {'value': value, 'owner': self.global_model.get('owner', 'tmp')} if default_value is True: self.model[path][str(index)]['value'] = value else: self._set_temp_value(path, index, value, self.global_model.get('owner', 'tmp')) - self.set_dependencies(path, value, index=index) - self.do_copy(path, value) + if not self.is_hidden(path, index): + changes.append(path) + self.set_dependencies(path, value, False, changes, index) + self.do_copy(path, index, value, changes) if leader_old_value is not undefined and len(leader_old_value) < len(value): # if leader and length is change, display/hide follower from follower's default value index = len(value) - 1 @@ -1086,6 +1155,8 @@ class Config: self.set_dependencies(follower_path, follower_value, None, index) except PropertiesOptionError: pass + for path in changes: + self.send_event('tiramisu-change', path) def _set_temp_value(self, path, index, value, owner): if index is not None: @@ -1124,64 +1195,71 @@ class Config: self.updates = [] self.temp.clear() self.model = data['model'] + for path in data['updates']: + self.send_event('tiramisu-change', path) def test_value(self, path: str, index: Optional[int], - value: Any, - remote: bool): + value: Any) -> bool: if isinstance(value, list): for val in value: - if not self.test_value(path, index, val, remote): - if not 'demoting_error_warning' in self.global_model.get('properties', ['demoting_error_warning']): - raise ValueError('value {} is not valid for {}'.format(value, path)) + if not self._test_value(path, index, val): + # when a value is invalid, all are invalid return False return True + return not self._test_value(path, index, value) + + def _test_value(self, + path: str, + index:Optional[int], + value: Any) -> bool: + if not path in self.form or not 'pattern' in self.form[path]: + return True + if value is None or value is '': + match = True else: - if value is None: - match = True + if isinstance(value, int): + value = str(value) + match = self.form[path]['pattern'].search(value) is not None + if not path in self.temp: + self.temp[path] = {} + if index is None: + if not match: + self.temp[path]['invalid'] = True + self.temp[path]['error'] = ['invalid value'] else: - if isinstance(value, int): - value = str(value) - match = self.form[path]['pattern'].search(value) is not None - if not remote: - if not match: - if index is None: - self.temp.setdefault(path, {})['error'] = [''] - else: - self.temp.setdefault(path, {}) - self.temp[path].setdefault(str(index), {}) - self.temp[path][str(index)]= [''] - elif index is not None and 'error' in self.temp.get(path, {}).get(str(index), {}): - del self.temp[path][str(index)]['error'] - elif 'error' in self.temp.get(path, {}): - del self.temp[path]['error'] - if index is not None and 'error' in self.model.get(path, {}).get(str(index), {}): - del self.model[path][str(index)]['error'] - elif 'error' in self.model.get(path, {}): - del self.model[path]['error'] - if not match and not 'demoting_error_warning' in self.global_model.get('properties', ['demoting_error_warning']): - raise ValueError('value {} is not valid for {}'.format(value, path)) - return match + self.temp[path]['invalid'] = False + else: + if not str(index) in self.temp[path]: + self.temp[path][str(index)] = {} + if not match: + self.temp[path][str(index)]['invalid'] = True + self.temp[path][str(index)]['error'] = ['invalid value'] + else: + self.temp[path][str(index)]['invalid'] = False + return match def set_dependencies(self, path: str, ori_value: Any, - force_hide: bool=False, + force_hide: bool, + changes: List, index: Optional[int]=None) -> None: dependencies = self.form.get(path, {}).get('dependencies', {}) if dependencies: if not isinstance(ori_value, list): - self._set_dependencies(path, ori_value, dependencies, force_hide, index) + self._set_dependencies(path, ori_value, dependencies, force_hide, changes, index) else: for idx, ori_val in enumerate(ori_value): - self._set_dependencies(path, ori_val, dependencies, force_hide, idx) + self._set_dependencies(path, ori_val, dependencies, force_hide, changes, idx) def _set_dependencies(self, path: str, ori_value: Any, dependencies: Dict, force_hide: bool, + changes: List, index: Optional[int]) -> None: if ori_value in dependencies['expected']: expected = dependencies['expected'][ori_value] @@ -1191,16 +1269,34 @@ class Config: expected_actions = expected.get(action) if expected_actions: if force_hide: - display = False + display = True else: display = action == 'show' for expected_path in expected_actions: + if expected_path not in self.temp: + self.temp[expected_path] = {} + old_hidden = self.is_hidden(expected_path, + index) + leader_path = None if index is not None: - self.temp.setdefault(expected_path, {}).setdefault(str(index), {})['display'] = display + if str(index) not in self.temp[expected_path]: + self.temp[expected_path][str(index)] = {} + self.temp[expected_path][str(index)]['display'] = display else: - self.temp.setdefault(expected_path, {})['display'] = display + self.temp[expected_path]['display'] = display + schema = self.get_schema(expected_path) + if schema['type'] == 'array': + leader_path = next(iter(schema['properties'].keys())) + if leader_path not in self.temp: + self.temp[leader_path] = {} + self.temp[leader_path]['display'] = display + if old_hidden == display: + if expected_path not in changes: + changes.append(expected_path) + if leader_path not in changes: + changes.append(leader_path) value = self.get_value(expected_path, index) - self.set_dependencies(expected_path, value, not display, index) + self.set_dependencies(expected_path, value, not display, changes, index) def set_not_equal(self, path: str, @@ -1252,33 +1348,38 @@ class Config: for opt_ in equal: display_equal.append('"' + self.get_schema(opt_)['title'] + '"') display_equal = display_list(display_equal) - #if opt_ == path: - # msg_ = msgcurr.format(display_equal) - #else: msg_ = msg.format(display_equal) - for path_ in not_equal['options'] + [path]: - if path_ not in self.model: - self.model[path_] = {} - model = self.model[path_] + is_demoting_error_warning = 'demoting_error_warning' in self.global_model.get('properties', []) + if warnings_only or is_demoting_error_warning: + paths = not_equal['options'] + [path] + else: + paths = [path] + for path_ in paths: + if path_ not in self.temp: + self.temp[path_] = {} + temp = self.temp[path_] if index is not None: - if str(index) not in model: - model[str(index)] = {} - model = model[str(index)] + if str(index) not in temp: + temp[str(index)] = {} + temp = temp[str(index)] if warnings_only: - model.setdefault('warnings', []).append(msg_) + temp['hasWarnings'] = True + temp.setdefault('warnings', []).append(msg_) else: - if 'demoting_error_warning' not in self.global_model.get('properties', []): - raise ValueError(msg_) - model.setdefault('error', []).append(msg_) + if not is_demoting_error_warning: + raise ValueError(msg) + temp['invalid'] = True + temp.setdefault('error', []).append(msg_) def do_copy(self, path: str, - value: Any) -> None: + index: Optional[int], + value: Any, + changes: List) -> None: copy = self.form.get(path, {}).get('copy') if copy: for opt in copy: - # FIXME follower! - owner = self.get_owner(opt, None) + owner = self.get_owner(opt, index) if owner == 'default': # do not change in self.temp, it's default value if self.model[opt]['value'] != value: @@ -1286,27 +1387,38 @@ class Config: value = value.copy() # self.model[opt]['value'] = value remote = self.form.get(opt, {}).get('remote', False) - self.updates_value('modify', opt, None, value, remote, True) + self.updates_value('modify', opt, index, value, remote, True) + if not self.is_hidden(opt, index) and opt not in changes: + changes.append(opt) + def _check_raises_warnings(self, path, index, value, type, withwarning=True): - model = self.model.get(path, {}) - if index is not None: - model = model.get(str(index), {}) - for err in model.get('error', []): - if 'demoting_error_warning' in self.global_model.get('properties', []): - warnings.warn_explicit(ValueErrorWarning(value, - type, - Option(path, path), - '{0}'.format(err), - index), - ValueErrorWarning, - 'Option', 0) - else: - del model['error'] - raise ValueError(err) + subconfig_value = self.option(path, index).value + if not subconfig_value.valid(): + is_demoting_error_warning = 'demoting_error_warning' in self.global_model.get('properties', []) + for err in subconfig_value.error_message(): + if is_demoting_error_warning: + warnings.warn_explicit(ValueErrorWarning(value, + type, + Option(path, path), + '{0}'.format(err), + index), + ValueErrorWarning, + 'Option', 0) + else: + if path not in self.temp: + self.temp[path] = {} + if index is None: + obj = self.temp[path] + else: + if str(index) not in self.temp[path]: + self.temp[path][str(index)] = {} + obj = self.temp[path][str(index)] + obj['invalid'] = False + raise ValueError(err) - if withwarning and model.get('warnings'): - for warn in model.get('warnings'): + if withwarning and subconfig_value.warning(): + for warn in subconfig_value.warning_message(): warnings.warn_explicit(ValueErrorWarning(value, type, Option(path, path), @@ -1318,3 +1430,8 @@ class Config: def send_data(self, updates): raise NotImplementedError('please implement send_data method') + + def send_event(self, + evt: str, + path: str): + pass