mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 02:47:49 +00:00
[bug] better handling of empty values for audit log (#13134)
* [bug] better handling of empty values for audit log * reverse unrelated commits
This commit is contained in:
1 parent
e6bc21b66f
commit
d96174cddb
2 files changed
+52
-8
No files matched your search
@@ -119,6 +119,20 @@ def drop_orphan_currency_changes(instance, changes: dict) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def is_unchanged_value(value) -> bool:
|
||||
"""Return True if a recorded [old, new] change pair does not represent a real change.
|
||||
|
||||
None and '' are treated as equivalent, as nullable string fields (e.g. Part.IPN)
|
||||
may hold either value for "empty" - e.g. a NULL value saved from a form as ''.
|
||||
"""
|
||||
if not (isinstance(value, (list, tuple)) and len(value) == 2):
|
||||
return False
|
||||
|
||||
old, new = (None if v == '' else v for v in value)
|
||||
|
||||
return old == new
|
||||
|
||||
|
||||
def is_concrete_field(model, field_name: str) -> bool:
|
||||
"""Return True if field_name is a concrete field on the model (or is unknown)."""
|
||||
try:
|
||||
@@ -224,9 +238,10 @@ class AuditLogEntryManager(LogEntryManager):
|
||||
def log_create(self, instance, force_log: bool = False, **kwargs):
|
||||
"""Create a new log entry for the provided instance.
|
||||
|
||||
For updates, fields whose recorded old and new values are identical are dropped.
|
||||
These occur when the in-memory value differs from the database value only in type
|
||||
(e.g. an empty FieldFile vs. an empty string), and would otherwise add noise to every save.
|
||||
For updates, fields whose recorded old and new values are identical (or both empty,
|
||||
i.e. None or '') are dropped. These occur when the in-memory value differs from the
|
||||
database value only in type (e.g. an empty FieldFile vs. an empty string, or a NULL
|
||||
value saved from a form as ''), and would otherwise add noise to every save.
|
||||
|
||||
Updates which assign a query expression (e.g. F('completed') + 1) are deferred until
|
||||
after the save, so that the value actually written can be recorded
|
||||
@@ -264,11 +279,7 @@ class AuditLogEntryManager(LogEntryManager):
|
||||
changes = {
|
||||
field: value
|
||||
for field, value in changes.items()
|
||||
if not (
|
||||
isinstance(value, (list, tuple))
|
||||
and len(value) == 2
|
||||
and value[0] == value[1]
|
||||
)
|
||||
if not is_unchanged_value(value)
|
||||
}
|
||||
|
||||
if not changes and not force_log:
|
||||
|
||||
@@ -208,6 +208,39 @@ class AuditModelTest(AuditTestMixin, TestCase):
|
||||
self.assertNotIn('reference_int', entry.changes_dict)
|
||||
self.assertNotIn('metadata', entry.changes_dict)
|
||||
|
||||
def test_empty_values_equivalent(self):
|
||||
"""Changing a field between None and '' is not recorded as a change."""
|
||||
Part.objects.filter(pk=self.part.pk).update(IPN=None)
|
||||
self.part.refresh_from_db()
|
||||
|
||||
# Saving an empty value from a form ('') over NULL
|
||||
self.part.IPN = ''
|
||||
self.part.description = 'Updated'
|
||||
self.part.save()
|
||||
|
||||
entry = self.entries_for(self.part, action=AuditLogEntry.Action.UPDATE).last()
|
||||
self.assertEqual(entry.changes_dict, {'description': ['A part', 'Updated']})
|
||||
|
||||
# No entry is created if the only "change" is between empty values
|
||||
self.part.IPN = None
|
||||
self.part.save()
|
||||
|
||||
self.assertEqual(
|
||||
self.entries_for(self.part, action=AuditLogEntry.Action.UPDATE).count(), 1
|
||||
)
|
||||
|
||||
# A real change to or from an empty value is still recorded
|
||||
self.part.IPN = 'IPN-001'
|
||||
self.part.save()
|
||||
self.part.IPN = ''
|
||||
self.part.save()
|
||||
|
||||
entries = self.entries_for(self.part, action=AuditLogEntry.Action.UPDATE)
|
||||
self.assertEqual(
|
||||
[e.changes_dict['IPN'] for e in entries.order_by('pk')[1:]],
|
||||
[[None, 'IPN-001'], ['IPN-001', '']],
|
||||
)
|
||||
|
||||
def test_parent_link(self):
|
||||
"""Child entries are linked to the parent - and are retained after deletion."""
|
||||
line = PurchaseOrderLineItem.objects.create(
|
||||
|
||||
Reference in new issue
Block a user