diff --git a/ir/model.py b/ir/model.py index 2287a4a..54c552c 100755 --- a/ir/model.py +++ b/ir/model.py @@ -1606,8 +1606,53 @@ class Log(ResourceAccessMixin, ModelSQL, ModelView): return json.dumps(value, sort_keys=True, ensure_ascii=False) if value in (None, ''): return '' + if isinstance(value, str) and re.match(r'^-?\d+(\.\d+)?$', value): + value = value.rstrip('0').rstrip('.') + return value or '0' return str(value) + @staticmethod + def _singular_label(label): + if label.endswith('ies'): + return label[:-3] + 'y' + if label.endswith('s'): + return label[:-1] + return label + + def _format_nested_change_summary( + self, field_label, target_model, old_value, new_value): + pool = Pool() + Field = pool.get('ir.model.field') + lines = [] + if not isinstance(old_value, dict) or not isinstance(new_value, dict): + return lines + record_label = self._singular_label(field_label) + def record_sort_key(value): + try: + return int(value) + except (TypeError, ValueError): + return value + for record_id in sorted( + set(old_value) | set(new_value), key=record_sort_key): + record_old = old_value.get(record_id, {}) + record_new = new_value.get(record_id, {}) + if not isinstance(record_old, dict) or not isinstance( + record_new, dict): + continue + for child_field in sorted(set(record_old) | set(record_new)): + child_old = record_old.get(child_field) + child_new = record_new.get(child_field) + if child_old == child_new: + continue + child_label = Field.get_name(target_model, child_field) + lines.append('%s %s %s %s => %s' % ( + record_label, + record_id, + child_label, + self._format_change_value(child_old), + self._format_change_value(child_new))) + return lines + def get_change_summary(self, name): if self.event != 'write': return '' @@ -1628,6 +1673,14 @@ class Log(ResourceAccessMixin, ModelSQL, ModelView): field_label = Field.get_name(model, field_name) else: field_label = field_name + field = getattr(self.resource.__class__, field_name, None) + if field and hasattr(field, 'get_target'): + target_model = field.get_target().__name__ + nested_lines = self._format_nested_change_summary( + field_label, target_model, old_value, new_value) + if nested_lines: + lines.extend(nested_lines) + continue lines.append('%s: %s -> %s' % ( field_label, self._format_change_value(old_value), diff --git a/model/modelstorage.py b/model/modelstorage.py index 859ce8e..9e72b7c 100755 --- a/model/modelstorage.py +++ b/model/modelstorage.py @@ -221,6 +221,72 @@ class ModelStorage(Model): } return json.dumps(values, sort_keys=True, ensure_ascii=False) + @staticmethod + def _iter_log_commands(value): + if not isinstance(value, (list, tuple)): + return + for item in value: + if (isinstance(item, (list, tuple)) + and item and isinstance(item[0], str)): + command = item[0] + if len(item) >= 3: + yield command, item[1], item[2] + elif len(item) >= 2: + yield command, item[1], None + if value and all(isinstance(item, (list, tuple)) for item in value): + return + index = 0 + while index < len(value): + command = value[index] + if not isinstance(command, str): + index += 1 + continue + if command == 'write': + if index + 2 >= len(value): + break + yield command, value[index + 1], value[index + 2] + index += 3 + continue + if index + 1 >= len(value): + break + yield command, value[index + 1], None + index += 2 + + @classmethod + def _nested_log_values(cls, values): + nested_old = {} + nested_new = {} + for field_name, value in values.items(): + field = cls._fields.get(field_name) + if not field or not hasattr(field, 'get_target'): + continue + commands = list(cls._iter_log_commands(value) or []) + write_commands = [ + (ids, command_values) + for command, ids, command_values in commands + if command == 'write' and command_values] + if not write_commands: + continue + Target = field.get_target() + field_old = {} + field_new = {} + for ids, command_values in write_commands: + target_ids = [int(i) for i in ids] + field_names = list(sorted(command_values.keys())) + with without_check_access(): + rows = Target.read(target_ids, field_names) + for row in rows: + record_id = str(row['id']) + field_old.setdefault(record_id, {}).update({ + name: row.get(name) + for name in field_names + }) + field_new.setdefault(record_id, {}).update(command_values) + if field_old or field_new: + nested_old[field_name] = field_old + nested_new[field_name] = field_new + return nested_old, nested_new + @classmethod def create(cls, vlist): ''' @@ -304,11 +370,16 @@ class ModelStorage(Model): } for row in cls.read( [r.id for r in records], fields_names)} + nested_old, nested_new = cls._nested_log_values(values) for record in records: + record_old_values = old_values.get(record.id, {}) + record_new_values = values.copy() + record_old_values.update(nested_old) + record_new_values.update(nested_new) cls.log([record], 'write', ','.join(fields_names), old_values=cls._log_value_text( - old_values.get(record.id, {})), - new_values=cls._log_value_text(values)) + record_old_values), + new_values=cls._log_value_text(record_new_values)) ModelAccess.check(cls.__name__, 'write') ModelFieldAccess.check(cls.__name__, all_fields, 'write')