django-field-audit 1.2.8__tar.gz → 1.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-field-audit might be problematic. Click here for more details.

Files changed (31) hide show
  1. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/PKG-INFO +10 -2
  2. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/django_field_audit.egg-info/PKG-INFO +10 -2
  3. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/django_field_audit.egg-info/SOURCES.txt +9 -1
  4. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/__init__.py +1 -1
  5. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/auditors.py +1 -1
  6. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/setup.cfg +3 -1
  7. django_field_audit-1.2.9/tests/test_apps.py +18 -0
  8. django_field_audit-1.2.9/tests/test_auditors.py +238 -0
  9. django_field_audit-1.2.9/tests/test_bootstrap_field_audit_events.py +144 -0
  10. django_field_audit-1.2.9/tests/test_django_compat.py +148 -0
  11. django_field_audit-1.2.9/tests/test_field_audit.py +178 -0
  12. django_field_audit-1.2.9/tests/test_middleware.py +25 -0
  13. django_field_audit-1.2.9/tests/test_models.py +1424 -0
  14. django_field_audit-1.2.9/tests/test_utils.py +83 -0
  15. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/LICENSE +0 -0
  16. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/README.md +0 -0
  17. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/django_field_audit.egg-info/dependency_links.txt +0 -0
  18. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/django_field_audit.egg-info/top_level.txt +0 -0
  19. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/apps.py +0 -0
  20. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/const.py +0 -0
  21. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/field_audit.py +0 -0
  22. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/management/__init__.py +0 -0
  23. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/management/commands/__init__.py +0 -0
  24. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/management/commands/bootstrap_field_audit_events.py +0 -0
  25. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/middleware.py +0 -0
  26. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/migrations/0001_initial.py +0 -0
  27. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/migrations/0002_add_is_bootstrap_column.py +0 -0
  28. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/migrations/__init__.py +0 -0
  29. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/models.py +0 -0
  30. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/field_audit/utils.py +0 -0
  31. {django-field-audit-1.2.8 → django_field_audit-1.2.9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: django-field-audit
3
- Version: 1.2.8
3
+ Version: 1.2.9
4
4
  Summary: Audit Field Changes on Django Models
5
5
  Home-page: https://github.com/dimagi/django-field-audit
6
6
  Maintainer: Joel Miller
@@ -26,6 +26,14 @@ Classifier: Framework :: Django :: 4.2
26
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: license
34
+ Dynamic: maintainer
35
+ Dynamic: maintainer-email
36
+ Dynamic: summary
29
37
 
30
38
  # Audit Field Changes on Django Models
31
39
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: django-field-audit
3
- Version: 1.2.8
3
+ Version: 1.2.9
4
4
  Summary: Audit Field Changes on Django Models
5
5
  Home-page: https://github.com/dimagi/django-field-audit
6
6
  Maintainer: Joel Miller
@@ -26,6 +26,14 @@ Classifier: Framework :: Django :: 4.2
26
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: license
34
+ Dynamic: maintainer
35
+ Dynamic: maintainer-email
36
+ Dynamic: summary
29
37
 
30
38
  # Audit Field Changes on Django Models
31
39
 
@@ -19,4 +19,12 @@ field_audit/management/commands/__init__.py
19
19
  field_audit/management/commands/bootstrap_field_audit_events.py
20
20
  field_audit/migrations/0001_initial.py
21
21
  field_audit/migrations/0002_add_is_bootstrap_column.py
22
- field_audit/migrations/__init__.py
22
+ field_audit/migrations/__init__.py
23
+ tests/test_apps.py
24
+ tests/test_auditors.py
25
+ tests/test_bootstrap_field_audit_events.py
26
+ tests/test_django_compat.py
27
+ tests/test_field_audit.py
28
+ tests/test_middleware.py
29
+ tests/test_models.py
30
+ tests/test_utils.py
@@ -1,3 +1,3 @@
1
1
  from .field_audit import audit_fields # noqa: F401
2
2
 
3
- __version__ = "1.2.8"
3
+ __version__ = "1.2.9"
@@ -107,7 +107,7 @@ class SystemUserAuditor(BaseAuditor):
107
107
  try:
108
108
  # get owner of STDIN file on login sessions (e.g. SSH)
109
109
  output = check_output(["who", "-m"], stderr=DEVNULL)
110
- except CalledProcessError:
110
+ except (FileNotFoundError, CalledProcessError):
111
111
  self.has_who_bin = False
112
112
  else:
113
113
  if output:
@@ -6,7 +6,9 @@ license_file = LICENSE
6
6
 
7
7
  [flake8]
8
8
  max-line-length = 80
9
- exclude = ./build
9
+ exclude =
10
+ ./build
11
+ .venv
10
12
 
11
13
  [egg_info]
12
14
  tag_build =
@@ -0,0 +1,18 @@
1
+ from django.apps import apps
2
+ from django.test import TestCase
3
+
4
+ from field_audit.auditors import audit_dispatcher
5
+
6
+
7
+ class TestFieldAuditConfig(TestCase):
8
+
9
+ def test_config_ready_sets_auditors(self):
10
+ # ensure it's not empty already
11
+ self.assertNotEqual([], audit_dispatcher.auditors)
12
+ try:
13
+ audit_dispatcher.auditors = []
14
+ apps.get_app_config("field_audit").ready()
15
+ self.assertNotEqual([], audit_dispatcher.auditors)
16
+ finally:
17
+ # reset to defaults
18
+ audit_dispatcher.setup_auditors()
@@ -0,0 +1,238 @@
1
+ from subprocess import CalledProcessError
2
+ from unittest.mock import patch
3
+
4
+ from django.conf import settings
5
+ from django.test import TestCase
6
+ from django.test.utils import override_settings
7
+
8
+ from field_audit.auditors import (
9
+ BaseAuditor,
10
+ RequestAuditor,
11
+ SystemUserAuditor,
12
+ audit_dispatcher,
13
+ )
14
+ from field_audit.models import (
15
+ USER_TYPE_PROCESS,
16
+ USER_TYPE_REQUEST,
17
+ USER_TYPE_TTY,
18
+ )
19
+
20
+
21
+ class TestAuditDispatcher(TestCase):
22
+
23
+ def setUp(self):
24
+ # reset the auditor chain to default values
25
+ audit_dispatcher.setup_auditors()
26
+
27
+ def test_audit_dispatcher_default_auditor_chain(self):
28
+ with self.assertRaises(AttributeError):
29
+ getattr(settings, "FIELD_AUDIT_AUDITORS")
30
+ request_auditor, sysuser_auditor = audit_dispatcher.auditors
31
+ self.assertIsInstance(request_auditor, RequestAuditor)
32
+ self.assertIsInstance(sysuser_auditor, SystemUserAuditor)
33
+
34
+ @override_settings(
35
+ FIELD_AUDIT_AUDITORS=[
36
+ "tests.test_auditors.CustomAuditor1",
37
+ "tests.test_auditors.CustomAuditor2",
38
+ "tests.test_auditors.CustomAuditor3",
39
+ ],
40
+ )
41
+ def test_audit_dispatcher_auditor_chain_is_configurable(self):
42
+ audit_dispatcher.setup_auditors()
43
+ one, two, three = audit_dispatcher.auditors
44
+ self.assertIsInstance(one, CustomAuditor1)
45
+ self.assertIsInstance(two, CustomAuditor2)
46
+ self.assertIsInstance(three, CustomAuditor3)
47
+
48
+ @override_settings(FIELD_AUDIT_AUDITORS=[])
49
+ def test_audit_dispatcher_setting_empty_auditor_chain_clears_auditors(self):
50
+ audit_dispatcher.setup_auditors()
51
+ self.assertEqual([], audit_dispatcher.auditors)
52
+
53
+ @override_settings(FIELD_AUDIT_AUDITORS=["tests.models.Flight"])
54
+ def test_audit_dispatcher_custom_auditors_must_subclass_baseauditor(self):
55
+ from .models import Flight
56
+ self.assertFalse(issubclass(Flight, BaseAuditor))
57
+ with self.assertRaises(ValueError):
58
+ audit_dispatcher.setup_auditors()
59
+
60
+ def test_audit_dispatcher_chain(self):
61
+ aud1 = MockAuditor(True)
62
+ aud2 = MockAuditor(True)
63
+ chain = [aud1, aud2]
64
+
65
+ # aud1 hits, aud2 never called
66
+ with patch.object(audit_dispatcher, "auditors", chain):
67
+ change_context = audit_dispatcher.dispatch(object())
68
+ self.assertIs(change_context, aud1)
69
+ self.assertEqual(aud1.dispatched, 1)
70
+ self.assertEqual(aud2.dispatched, 0)
71
+
72
+ # aud1 misses, aud2 hits
73
+ aud1.reset(False)
74
+ with patch.object(audit_dispatcher, "auditors", chain):
75
+ change_context = audit_dispatcher.dispatch(object())
76
+ self.assertIs(change_context, aud2)
77
+ self.assertEqual(aud1.dispatched, 1)
78
+ self.assertEqual(aud2.dispatched, 1)
79
+
80
+ # both miss
81
+ aud1.reset(False)
82
+ aud2.reset(False)
83
+ with patch.object(audit_dispatcher, "auditors", chain):
84
+ change_context = audit_dispatcher.dispatch(object())
85
+ self.assertIsNone(change_context)
86
+ self.assertEqual(aud1.dispatched, 1)
87
+ self.assertEqual(aud2.dispatched, 1)
88
+
89
+
90
+ class CustomAuditor1(BaseAuditor):
91
+ pass
92
+
93
+
94
+ class CustomAuditor2(BaseAuditor):
95
+ pass
96
+
97
+
98
+ class CustomAuditor3(BaseAuditor):
99
+ pass
100
+
101
+
102
+ class MockAuditor:
103
+
104
+ def __init__(self, enabled):
105
+ self.reset(enabled)
106
+
107
+ def reset(self, enabled):
108
+ self.dispatched = 0
109
+ self.enabled = enabled
110
+
111
+ def change_context(self, request):
112
+ self.dispatched += 1
113
+ return self if self.enabled else None
114
+
115
+
116
+ class TestBaseAuditor(TestCase):
117
+
118
+ def test_baseauditor_change_context_raises_notimplementederror(self):
119
+ with self.assertRaises(NotImplementedError):
120
+ BaseAuditor().change_context(object())
121
+
122
+
123
+ class TestSystemUserAuditor(TestCase):
124
+
125
+ def setUp(self):
126
+ super().setUp()
127
+ self.auditor = SystemUserAuditor()
128
+
129
+ def test_systemuserauditor_change_context_returns_sys_value_for_request(self): # noqa: E501
130
+ def user(*args, **kw):
131
+ return b"test ..."
132
+ with patch("field_audit.auditors.check_output", side_effect=user):
133
+ self.assertEqual(
134
+ {"user_type": USER_TYPE_TTY, "username": "test"},
135
+ self.auditor.change_context(object()),
136
+ )
137
+
138
+ def _patch_system_getters_and_validate(self, fake_output, change_context):
139
+ kwargs = {"side_effect": fake_output}
140
+ user = None if change_context is None else change_context["username"]
141
+ with (
142
+ patch("field_audit.auditors.check_output", **kwargs) as chk_out,
143
+ patch("field_audit.auditors.getuser", return_value=user) as getuser,
144
+ ):
145
+ audit_info = self.auditor.change_context(None)
146
+ if isinstance(audit_info, dict):
147
+ self.assertEqual(change_context, audit_info)
148
+ else:
149
+ self.assertIsNone(audit_info)
150
+ return chk_out, getuser
151
+
152
+ def test_systemuserauditor_change_context_tolerates_invalid_who_output(self): # noqa: E501
153
+ def bogus(*args, **kw):
154
+ return b"\xb4"
155
+ ch_by = {"user_type": USER_TYPE_PROCESS, "username": "alice"}
156
+ chk_out, getuser = self._patch_system_getters_and_validate(bogus, ch_by)
157
+ chk_out.assert_called_once()
158
+ getuser.assert_called_once()
159
+
160
+ def test_systemuserauditor_change_context_tolerates_empty_who_output(self):
161
+ def empty(*args, **kw):
162
+ return b""
163
+ ch_by = {"user_type": USER_TYPE_PROCESS, "username": "bob"}
164
+ chk_out, getuser = self._patch_system_getters_and_validate(empty, ch_by)
165
+ chk_out.assert_called_once()
166
+ getuser.assert_called_once()
167
+
168
+ def test_systemuserauditor_remembers_who_error(self):
169
+ def fail(*args, **kw):
170
+ raise CalledProcessError(1, [], "")
171
+ ch_by = {"user_type": USER_TYPE_PROCESS, "username": "carlos"}
172
+ # round 1
173
+ chk_out, getuser = self._patch_system_getters_and_validate(fail, ch_by)
174
+ chk_out.assert_called_once()
175
+ getuser.assert_called_once()
176
+ # round 2
177
+ chk_out, getuser = self._patch_system_getters_and_validate(fail, ch_by)
178
+ chk_out.assert_not_called()
179
+ getuser.assert_called_once()
180
+
181
+ def test_systemuserauditor_handles_missing_who_bin(self):
182
+ def fail(*args, **kw):
183
+ raise FileNotFoundError()
184
+ ch_by = {"user_type": USER_TYPE_PROCESS, "username": "carlos"}
185
+ chk_out, getuser = self._patch_system_getters_and_validate(fail, ch_by)
186
+ chk_out.assert_called_once()
187
+ getuser.assert_called_once()
188
+
189
+ def test_systemuserauditor_change_context_returns_tty_user_on_who_output(self): # noqa: E501
190
+ def mock(*args, **kw):
191
+ return b"eve ttys000 May 8 17:04 (localhost)"
192
+ ch_by = {"user_type": USER_TYPE_TTY, "username": "eve"}
193
+ chk_out, getuser = self._patch_system_getters_and_validate(mock, ch_by)
194
+ chk_out.assert_called_once()
195
+ getuser.assert_not_called()
196
+
197
+ def test_systemuserauditor_change_context_returns_none_if_all_else_fails(self): # noqa: E501
198
+ def empty(*args, **kw):
199
+ return b""
200
+ chk_out, getuser = self._patch_system_getters_and_validate(empty, None)
201
+ chk_out.assert_called_once()
202
+ getuser.assert_called_once()
203
+
204
+
205
+ class TestRequestAuditor(TestCase):
206
+
207
+ def setUp(self):
208
+ self.request = AuthedRequest()
209
+ self.auditor = RequestAuditor()
210
+
211
+ def test_requestauditor_change_context(self):
212
+ self.assertEqual(
213
+ {
214
+ "user_type": USER_TYPE_REQUEST,
215
+ "username": self.request.user.username,
216
+ },
217
+ self.auditor.change_context(self.request),
218
+ )
219
+
220
+ def test_requestauditor_change_context_returns_none_without_request(self):
221
+ self.assertIsNone(self.auditor.change_context(None))
222
+
223
+ def test_requestauditor_change_context_returns_value_for_unauthorized_req(self): # noqa: E501
224
+ self.request.deauth()
225
+ self.assertEqual({}, self.auditor.change_context(self.request))
226
+
227
+
228
+ class AuthedRequest:
229
+
230
+ class User:
231
+ username = "test@example.com"
232
+ is_authenticated = True
233
+
234
+ def __init__(self):
235
+ self.user = self.User()
236
+
237
+ def deauth(self):
238
+ self.user.is_authenticated = False
@@ -0,0 +1,144 @@
1
+ from contextlib import contextmanager
2
+ from io import StringIO
3
+ from unittest.mock import ANY, patch
4
+
5
+ from django.core.management import CommandError, call_command
6
+ from django.db import models
7
+ from django.test import TestCase
8
+
9
+ from field_audit.const import BOOTSTRAP_BATCH_SIZE
10
+ from field_audit.field_audit import get_audited_models
11
+ from field_audit.management.commands import (
12
+ bootstrap_field_audit_events as bootstrap,
13
+ )
14
+ from field_audit.models import AuditEvent
15
+ from tests.models import PkAuto
16
+
17
+
18
+ @contextmanager
19
+ def restore_command_models(command_class):
20
+ backup = command_class.models
21
+ yield
22
+ command_class.models = backup
23
+
24
+
25
+ class TestCommand(TestCase):
26
+
27
+ command = "bootstrap_field_audit_events"
28
+
29
+ def setUp(self):
30
+ super().setUp()
31
+ # create some records for bootstrapping
32
+ for _ in range(2):
33
+ PkAuto.objects.create()
34
+ # clear the "create" audit events
35
+ AuditEvent.objects.all().delete()
36
+
37
+ def test_setup_models_populates_models_dict(self):
38
+ with restore_command_models(bootstrap.Command):
39
+ bootstrap.Command.models = {}
40
+ bootstrap.Command.setup_models()
41
+ self.assertEqual(
42
+ set(get_audited_models()),
43
+ set(bootstrap.Command.models.values()),
44
+ )
45
+
46
+ def test_setup_models_uses_model_name(self):
47
+ with restore_command_models(bootstrap.Command):
48
+ bootstrap.Command.models = {}
49
+ bootstrap.Command.setup_models()
50
+ self.assertIs(PkAuto, bootstrap.Command.models["PkAuto"])
51
+
52
+ def test_setup_models_uses_app_name_to_prevent_model_name_collisions(self):
53
+ with restore_command_models(bootstrap.Command):
54
+ class ModelX(models.Model):
55
+ pass
56
+ bootstrap.Command.models = {"PkAuto": ModelX}
57
+ patch_kw = {"return_value": {PkAuto: "cls"}}
58
+ with patch.object(bootstrap, "get_audited_models", **patch_kw):
59
+ bootstrap.Command.setup_models()
60
+ self.assertEqual(
61
+ {"tests.ModelX": ModelX, "tests.PkAuto": PkAuto},
62
+ bootstrap.Command.models,
63
+ )
64
+
65
+ def test_setup_models_crashes_on_verbose_model_name_collision(self):
66
+
67
+ class ModelY(models.Model):
68
+ pass
69
+
70
+ class ModelZ(models.Model):
71
+ pass
72
+
73
+ ModelZ.__name__ = ModelY.__name__
74
+ with restore_command_models(bootstrap.Command):
75
+ patch_kw = {"return_value": {ModelY: "y", ModelZ: "z"}}
76
+ with (
77
+ patch.object(bootstrap, "get_audited_models", **patch_kw),
78
+ self.assertRaises(bootstrap.InvalidModelState),
79
+ ):
80
+ bootstrap.Command.setup_models()
81
+
82
+ def test_bootstrap_uses_default_batch_size(self):
83
+ with patch.object(AuditEvent, "bootstrap_top_up") as mock:
84
+ self.quiet_command("top-up", "PkAuto")
85
+ mock.assert_called_once_with(
86
+ PkAuto, ANY, batch_size=BOOTSTRAP_BATCH_SIZE,
87
+ )
88
+
89
+ def test_bootstrap_allows_custom_batch_size(self):
90
+ with patch.object(AuditEvent, "bootstrap_top_up") as mock:
91
+ self.quiet_command("top-up", "--batch-size", "1", "PkAuto")
92
+ mock.assert_called_once_with(PkAuto, ANY, batch_size=1)
93
+
94
+ def test_bootstrap_disables_batching_for_batch_size_zero(self):
95
+ with patch.object(AuditEvent, "bootstrap_top_up") as mock:
96
+ self.quiet_command("top-up", "--batch-size", "0", "PkAuto")
97
+ mock.assert_called_once_with(PkAuto, ANY, batch_size=None)
98
+
99
+ def test_bootstrap_crashes_for_negative_batch_size(self):
100
+ with self.assertRaises(CommandError):
101
+ self.quiet_command("top-up", "--batch-size", "-1", "PkAuto")
102
+
103
+ def test_bootstrap_crashes_early_if_model_has_invalid_fields(self):
104
+ with (
105
+ patch.object(PkAuto, AuditEvent.ATTACH_FIELD_NAMES_AT, []),
106
+ self.assertRaises(CommandError),
107
+ ):
108
+ self.quiet_command("init", "PkAuto")
109
+
110
+ def test_bootstrap_init_creates_audit_events_for_all_model_records(self):
111
+ self.quiet_command("init", "PkAuto")
112
+ self.assertEqual(
113
+ set(PkAuto.objects.all().values_list("pk", flat=True)),
114
+ set(
115
+ AuditEvent.objects.by_model(PkAuto)
116
+ .filter(is_bootstrap=True)
117
+ .values_list("object_pk", flat=True)
118
+ ),
119
+ )
120
+
121
+ def test_bootstrap_top_up_creates_audit_events_for_some_model_records(self):
122
+ need_bootstrap = set(PkAuto.objects.all().values_list("pk", flat=True))
123
+ # generate two more models (generates two "create" audit events)
124
+ for _ in range(2):
125
+ PkAuto.objects.create()
126
+ # convert one of them to a bootstrap event
127
+ event = AuditEvent.objects.first()
128
+ event.is_create = False
129
+ event.is_bootstrap = True
130
+ event.save()
131
+ pre_top_up_event_ids = set(
132
+ AuditEvent.objects.all().values_list("id", flat=True)
133
+ )
134
+ self.quiet_command("top-up", "PkAuto")
135
+ self.assertEqual(
136
+ need_bootstrap,
137
+ set(
138
+ AuditEvent.objects.exclude(id__in=pre_top_up_event_ids)
139
+ .values_list("object_pk", flat=True)
140
+ ),
141
+ )
142
+
143
+ def quiet_command(self, *args, **kw):
144
+ return call_command(self.command, *args, stdout=StringIO(), **kw)
@@ -0,0 +1,148 @@
1
+ import django
2
+ from django.test import TestCase
3
+
4
+ from field_audit.models import AuditEvent
5
+
6
+ from .models import ModelWithValueOnSave, SimpleModel
7
+
8
+
9
+ class TestAuditedDbWrites(TestCase):
10
+
11
+ def test_model_delete_is_audited(self):
12
+ self.assertNoAuditEvents()
13
+ instance = SimpleModel.objects.create(id=0, value='test')
14
+ AuditEvent.objects.all().delete() # delete the create audit event
15
+ instance.delete()
16
+ self.assertAuditEvent(
17
+ is_delete=True,
18
+ delta={"id": {"old": 0}, "value": {"old": 'test'}},
19
+ )
20
+
21
+ def test_model_delete_with_value_on_save_is_audited(self):
22
+ self.assertNoAuditEvents()
23
+ instance = ModelWithValueOnSave.objects.create(id=0)
24
+ AuditEvent.objects.all().delete() # delete the create audit event
25
+ instance.delete()
26
+ self.assertAuditEvent(
27
+ is_delete=True,
28
+ delta={'id': {'old': 0}, 'save_count': {'old': 1}},
29
+ )
30
+
31
+ def test_model_save_is_audited(self):
32
+ self.assertNoAuditEvents()
33
+ SimpleModel(id=0).save()
34
+ self.assertAuditEvent(
35
+ is_create=True,
36
+ delta={"id": {"new": 0}, "value": {"new": None}},
37
+ )
38
+
39
+ def test_model_save_with_value_on_save_is_audited(self):
40
+ self.assertNoAuditEvents()
41
+ ModelWithValueOnSave.objects.create(id=0)
42
+ self.assertAuditEvent(
43
+ is_create=True,
44
+ delta={'id': {'new': 0}, 'save_count': {'new': 1}},
45
+ )
46
+
47
+ def test_model_multiple_saves_with_value_on_save_is_audited(self):
48
+ self.assertNoAuditEvents()
49
+ instance = ModelWithValueOnSave.objects.create(id=0)
50
+ AuditEvent.objects.all().delete() # delete the create audit event
51
+ instance.value = 'update'
52
+ instance.save()
53
+ self.assertAuditEvent(
54
+ is_create=False,
55
+ delta={'save_count': {'old': 1, 'new': 2}},
56
+ )
57
+
58
+ def test_queryset_bulk_create_is_not_audited(self):
59
+ self.assertNoAuditEvents()
60
+ instance, = SimpleModel.objects.bulk_create([SimpleModel(id=1)])
61
+ instance.refresh_from_db()
62
+ self.assertIsNotNone(instance.id)
63
+ self.assertNoAuditEvents()
64
+
65
+ def test_queryset_bulk_update_is_not_audited(self):
66
+ instance = SimpleModel.objects.create(id=1)
67
+ AuditEvent.objects.all().delete() # delete the create audit event
68
+ self.assertIsNone(instance.value)
69
+ instance.value = "test"
70
+ updates = SimpleModel.objects.bulk_update([instance], ["value"])
71
+ if django.VERSION[0] < 4:
72
+ expected_updates = None
73
+ else:
74
+ expected_updates = 1
75
+ self.assertEqual(expected_updates, updates)
76
+ instance.refresh_from_db()
77
+ self.assertEqual("test", instance.value)
78
+ self.assertNoAuditEvents()
79
+
80
+ def test_queryset_create_is_audited(self):
81
+ self.assertNoAuditEvents()
82
+ instance = SimpleModel.objects.create()
83
+ self.assertAuditEvent(
84
+ is_create=True,
85
+ delta={"id": {"new": instance.id}, "value": {"new": None}},
86
+ )
87
+
88
+ def test_queryset_delete_is_not_audited(self):
89
+ self.assertNoAuditEvents()
90
+ instance = SimpleModel.objects.create()
91
+ AuditEvent.objects.all().delete() # delete the create audit event
92
+ instance.refresh_from_db()
93
+ self.assertIsNotNone(instance.id)
94
+ SimpleModel.objects.all().delete()
95
+ self.assertNoAuditEvents()
96
+
97
+ def test_queryset_get_or_create_is_audited(self):
98
+ self.assertNoAuditEvents()
99
+ self.assertEqual([], list(SimpleModel.objects.all()))
100
+ SimpleModel.objects.get_or_create(id=0)
101
+ self.assertAuditEvent(
102
+ is_create=True,
103
+ delta={"id": {"new": 0}, "value": {"new": None}},
104
+ )
105
+
106
+ def test_queryset_update_is_not_audited(self):
107
+ self.assertNoAuditEvents()
108
+ instance = SimpleModel.objects.create()
109
+ AuditEvent.objects.all().delete() # delete the create audit event
110
+ SimpleModel.objects.all().update(value="test")
111
+ instance.refresh_from_db()
112
+ self.assertEqual("test", instance.value)
113
+ self.assertNoAuditEvents()
114
+
115
+ def test_queryset_update_or_create_is_audited_on_create(self):
116
+ self.assertNoAuditEvents()
117
+ self.assertEqual([], list(SimpleModel.objects.all()))
118
+ response = SimpleModel.objects.update_or_create({"value": "test"}, id=0)
119
+ instance, x = response
120
+ self.assertEqual(0, instance.id)
121
+ self.assertEqual("test", instance.value)
122
+ self.assertAuditEvent(
123
+ is_create=True,
124
+ delta={"id": {"new": 0}, "value": {"new": "test"}},
125
+ )
126
+
127
+ def test_queryset_update_or_create_is_audited_on_update(self):
128
+ self.assertNoAuditEvents()
129
+ instance = SimpleModel.objects.create()
130
+ AuditEvent.objects.all().delete() # delete the create audit event
131
+ self.assertEqual([instance], list(SimpleModel.objects.all()))
132
+ self.assertIsNone(instance.value)
133
+ SimpleModel.objects.update_or_create({"value": "test"}, id=instance.id)
134
+ instance.refresh_from_db()
135
+ self.assertEqual("test", instance.value)
136
+ self.assertAuditEvent(
137
+ is_create=False,
138
+ is_delete=False,
139
+ delta={"value": {"old": None, "new": "test"}},
140
+ )
141
+
142
+ def assertNoAuditEvents(self):
143
+ self.assertEqual([], list(AuditEvent.objects.all()))
144
+
145
+ def assertAuditEvent(self, **kwargs):
146
+ event, = AuditEvent.objects.all()
147
+ for name, value in kwargs.items():
148
+ self.assertEqual(value, getattr(event, name))