Better leadership support

This commit is contained in:
Emmanuel Garette 2019-07-25 15:22:00 +02:00
parent 44e17aa4c6
commit 4e797674ef
1 changed files with 226 additions and 109 deletions

View File

@ -339,6 +339,16 @@ class TiramisuOptionValue(_Value):
self._validate(type_, val) self._validate(type_, val)
else: else:
self._validate(type_, value) 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.config.modify_value(self.path,
self.index, self.index,
value, value,
@ -369,6 +379,62 @@ class TiramisuOptionValue(_Value):
value = self.schema.get('value') value = self.schema.get('value')
return 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: class _Option:
def list(self, def list(self,
@ -376,28 +442,33 @@ class _Option:
recursive=False): recursive=False):
if type not in ['all', 'option', 'optiondescription']: if type not in ['all', 'option', 'optiondescription']:
raise Exception('unknown list type {}'.format(type)) 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 not self.config.is_hidden(path, None):
if schema['type'] in ['object', 'array']: if subschema['type'] in ['object', 'array']:
if type in ['all', 'optiondescription']: if type in ['all', 'optiondescription']:
yield TiramisuOptionDescription(self.config, yield TiramisuOptionDescription(self.config,
schema, subschema,
self.form, self.form,
self.temp, self.temp,
path) path)
if recursive: if recursive:
yield from TiramisuOptionDescription(self.config, yield from TiramisuOptionDescription(self.config,
schema, subschema,
self.form, self.form,
self.temp, self.temp,
path).list(type, recursive) path).list(type, recursive)
elif type in ['all', 'option']: elif type in ['all', 'option']:
yield TiramisuOption(self.config, yield TiramisuOption(self.config,
schema, subschema,
self.form, self.form,
self.temp, self.temp,
path, path,
self.index) 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): class TiramisuOptionDescription(_Option):
@ -680,15 +751,10 @@ class Config:
remote: bool, remote: bool,
leader_old_value: Any) -> None: leader_old_value: Any) -> None:
schema = self.get_schema(path) schema = self.get_schema(path)
if value and isinstance(value, list) and undefined in value: has_undefined = value is not None and isinstance(value, list) and undefined in value
new_value = schema.get('defaultmulti') new_value = schema.get('defaultmulti')
if remote: if not remote:
for idx, val in enumerate(value): if has_undefined:
self.manage_updates('modify',
path,
idx,
val)
else:
while undefined in value: while undefined in value:
undefined_index = value.index(undefined) undefined_index = value.index(undefined)
schema_value = schema.get('value', []) schema_value = schema.get('value', [])
@ -707,15 +773,14 @@ class Config:
path, path,
index, index,
value) value)
else:
if not remote: elif has_undefined:
self.updates_value('modify', for idx, val in enumerate(value):
self.manage_updates('modify',
path, path,
index, idx,
value, val)
remote, else:
False,
leader_old_value)
self.manage_updates('modify', self.manage_updates('modify',
path, path,
index, index,
@ -980,8 +1045,8 @@ class Config:
if DEBUG: if DEBUG:
print('<===== send') print('<===== send')
print(self.updates) print(self.updates)
self.updates_data(self.send_data({'updates': self.updates, self.send_data({'updates': self.updates,
'model': self.model})) 'model': self.model})
def updates_value(self, def updates_value(self,
action: str, action: str,
@ -991,11 +1056,14 @@ class Config:
remote: bool, remote: bool,
default_value: bool=False, default_value: bool=False,
leader_old_value: Optional[Any]=undefined) -> None: 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): # 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 # return
if remote: if remote:
self.send() self.send()
else: 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]): if path in self.model and (index is None or str(index) in self.model[path]):
model = self.model[path] model = self.model[path]
if index is not None: if index is not None:
@ -1067,13 +1135,14 @@ class Config:
else: else:
self.set_not_equal(path, value, index) self.set_not_equal(path, value, index)
# set a value for a follower option # 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: if default_value is True:
self.model[path][str(index)]['value'] = value self.model[path][str(index)]['value'] = value
else: else:
self._set_temp_value(path, index, value, self.global_model.get('owner', 'tmp')) self._set_temp_value(path, index, value, self.global_model.get('owner', 'tmp'))
self.set_dependencies(path, value, index=index) if not self.is_hidden(path, index):
self.do_copy(path, value) 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_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 # if leader and length is change, display/hide follower from follower's default value
index = len(value) - 1 index = len(value) - 1
@ -1086,6 +1155,8 @@ class Config:
self.set_dependencies(follower_path, follower_value, None, index) self.set_dependencies(follower_path, follower_value, None, index)
except PropertiesOptionError: except PropertiesOptionError:
pass pass
for path in changes:
self.send_event('tiramisu-change', path)
def _set_temp_value(self, path, index, value, owner): def _set_temp_value(self, path, index, value, owner):
if index is not None: if index is not None:
@ -1124,64 +1195,71 @@ class Config:
self.updates = [] self.updates = []
self.temp.clear() self.temp.clear()
self.model = data['model'] self.model = data['model']
for path in data['updates']:
self.send_event('tiramisu-change', path)
def test_value(self, def test_value(self,
path: str, path: str,
index: Optional[int], index: Optional[int],
value: Any, value: Any) -> bool:
remote: bool):
if isinstance(value, list): if isinstance(value, list):
for val in value: for val in value:
if not self.test_value(path, index, val, remote): if not self._test_value(path, index, val):
if not 'demoting_error_warning' in self.global_model.get('properties', ['demoting_error_warning']): # when a value is invalid, all are invalid
raise ValueError('value {} is not valid for {}'.format(value, path))
return False return False
return True return True
else: return not self._test_value(path, index, value)
if value is None:
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 match = True
else: else:
if isinstance(value, int): if isinstance(value, int):
value = str(value) value = str(value)
match = self.form[path]['pattern'].search(value) is not None match = self.form[path]['pattern'].search(value) is not None
if not remote: if not path in self.temp:
if not match: self.temp[path] = {}
if index is None: if index is None:
self.temp.setdefault(path, {})['error'] = [''] if not match:
self.temp[path]['invalid'] = True
self.temp[path]['error'] = ['invalid value']
else: else:
self.temp.setdefault(path, {}) self.temp[path]['invalid'] = False
self.temp[path].setdefault(str(index), {}) else:
self.temp[path][str(index)]= [''] if not str(index) in self.temp[path]:
elif index is not None and 'error' in self.temp.get(path, {}).get(str(index), {}): self.temp[path][str(index)] = {}
del self.temp[path][str(index)]['error'] if not match:
elif 'error' in self.temp.get(path, {}): self.temp[path][str(index)]['invalid'] = True
del self.temp[path]['error'] self.temp[path][str(index)]['error'] = ['invalid value']
if index is not None and 'error' in self.model.get(path, {}).get(str(index), {}): else:
del self.model[path][str(index)]['error'] self.temp[path][str(index)]['invalid'] = False
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 return match
def set_dependencies(self, def set_dependencies(self,
path: str, path: str,
ori_value: Any, ori_value: Any,
force_hide: bool=False, force_hide: bool,
changes: List,
index: Optional[int]=None) -> None: index: Optional[int]=None) -> None:
dependencies = self.form.get(path, {}).get('dependencies', {}) dependencies = self.form.get(path, {}).get('dependencies', {})
if dependencies: if dependencies:
if not isinstance(ori_value, list): 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: else:
for idx, ori_val in enumerate(ori_value): 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, def _set_dependencies(self,
path: str, path: str,
ori_value: Any, ori_value: Any,
dependencies: Dict, dependencies: Dict,
force_hide: bool, force_hide: bool,
changes: List,
index: Optional[int]) -> None: index: Optional[int]) -> None:
if ori_value in dependencies['expected']: if ori_value in dependencies['expected']:
expected = dependencies['expected'][ori_value] expected = dependencies['expected'][ori_value]
@ -1191,16 +1269,34 @@ class Config:
expected_actions = expected.get(action) expected_actions = expected.get(action)
if expected_actions: if expected_actions:
if force_hide: if force_hide:
display = False display = True
else: else:
display = action == 'show' display = action == 'show'
for expected_path in expected_actions: 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: 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: 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) 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, def set_not_equal(self,
path: str, path: str,
@ -1252,33 +1348,38 @@ class Config:
for opt_ in equal: for opt_ in equal:
display_equal.append('"' + self.get_schema(opt_)['title'] + '"') display_equal.append('"' + self.get_schema(opt_)['title'] + '"')
display_equal = display_list(display_equal) display_equal = display_list(display_equal)
#if opt_ == path:
# msg_ = msgcurr.format(display_equal)
#else:
msg_ = msg.format(display_equal) msg_ = msg.format(display_equal)
for path_ in not_equal['options'] + [path]: is_demoting_error_warning = 'demoting_error_warning' in self.global_model.get('properties', [])
if path_ not in self.model: if warnings_only or is_demoting_error_warning:
self.model[path_] = {} paths = not_equal['options'] + [path]
model = self.model[path_]
if index is not None:
if str(index) not in model:
model[str(index)] = {}
model = model[str(index)]
if warnings_only:
model.setdefault('warnings', []).append(msg_)
else: else:
if 'demoting_error_warning' not in self.global_model.get('properties', []): paths = [path]
raise ValueError(msg_) for path_ in paths:
model.setdefault('error', []).append(msg_) if path_ not in self.temp:
self.temp[path_] = {}
temp = self.temp[path_]
if index is not None:
if str(index) not in temp:
temp[str(index)] = {}
temp = temp[str(index)]
if warnings_only:
temp['hasWarnings'] = True
temp.setdefault('warnings', []).append(msg_)
else:
if not is_demoting_error_warning:
raise ValueError(msg)
temp['invalid'] = True
temp.setdefault('error', []).append(msg_)
def do_copy(self, def do_copy(self,
path: str, path: str,
value: Any) -> None: index: Optional[int],
value: Any,
changes: List) -> None:
copy = self.form.get(path, {}).get('copy') copy = self.form.get(path, {}).get('copy')
if copy: if copy:
for opt in copy: for opt in copy:
# FIXME follower! owner = self.get_owner(opt, index)
owner = self.get_owner(opt, None)
if owner == 'default': if owner == 'default':
# do not change in self.temp, it's default value # do not change in self.temp, it's default value
if self.model[opt]['value'] != value: if self.model[opt]['value'] != value:
@ -1286,14 +1387,17 @@ class Config:
value = value.copy() value = value.copy()
# self.model[opt]['value'] = value # self.model[opt]['value'] = value
remote = self.form.get(opt, {}).get('remote', False) 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): def _check_raises_warnings(self, path, index, value, type, withwarning=True):
model = self.model.get(path, {}) subconfig_value = self.option(path, index).value
if index is not None: if not subconfig_value.valid():
model = model.get(str(index), {}) is_demoting_error_warning = 'demoting_error_warning' in self.global_model.get('properties', [])
for err in model.get('error', []): for err in subconfig_value.error_message():
if 'demoting_error_warning' in self.global_model.get('properties', []): if is_demoting_error_warning:
warnings.warn_explicit(ValueErrorWarning(value, warnings.warn_explicit(ValueErrorWarning(value,
type, type,
Option(path, path), Option(path, path),
@ -1302,11 +1406,19 @@ class Config:
ValueErrorWarning, ValueErrorWarning,
'Option', 0) 'Option', 0)
else: else:
del model['error'] 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) raise ValueError(err)
if withwarning and model.get('warnings'): if withwarning and subconfig_value.warning():
for warn in model.get('warnings'): for warn in subconfig_value.warning_message():
warnings.warn_explicit(ValueErrorWarning(value, warnings.warn_explicit(ValueErrorWarning(value,
type, type,
Option(path, path), Option(path, path),
@ -1318,3 +1430,8 @@ class Config:
def send_data(self, def send_data(self,
updates): updates):
raise NotImplementedError('please implement send_data method') raise NotImplementedError('please implement send_data method')
def send_event(self,
evt: str,
path: str):
pass