django-field-audit 1.2.8__tar.gz → 1.3.0__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 (33) hide show
  1. {django-field-audit-1.2.8/django_field_audit.egg-info → django_field_audit-1.3.0}/PKG-INFO +48 -2
  2. django-field-audit-1.2.8/PKG-INFO → django_field_audit-1.3.0/README.md +38 -29
  3. django-field-audit-1.2.8/README.md → django_field_audit-1.3.0/django_field_audit.egg-info/PKG-INFO +75 -0
  4. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/django_field_audit.egg-info/SOURCES.txt +11 -1
  5. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/__init__.py +1 -1
  6. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/auditors.py +1 -1
  7. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/field_audit.py +83 -0
  8. django_field_audit-1.3.0/field_audit/migrations/0003_alter_auditevent_change_context_and_more.py +32 -0
  9. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/models.py +53 -6
  10. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/setup.cfg +3 -1
  11. django_field_audit-1.3.0/tests/test_apps.py +18 -0
  12. django_field_audit-1.3.0/tests/test_auditors.py +238 -0
  13. django_field_audit-1.3.0/tests/test_bootstrap_field_audit_events.py +144 -0
  14. django_field_audit-1.3.0/tests/test_django_compat.py +148 -0
  15. django_field_audit-1.3.0/tests/test_field_audit.py +180 -0
  16. django_field_audit-1.3.0/tests/test_m2m.py +229 -0
  17. django_field_audit-1.3.0/tests/test_middleware.py +25 -0
  18. django_field_audit-1.3.0/tests/test_models.py +1451 -0
  19. django_field_audit-1.3.0/tests/test_utils.py +83 -0
  20. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/LICENSE +0 -0
  21. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/django_field_audit.egg-info/dependency_links.txt +0 -0
  22. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/django_field_audit.egg-info/top_level.txt +0 -0
  23. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/apps.py +0 -0
  24. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/const.py +0 -0
  25. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/management/__init__.py +0 -0
  26. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/management/commands/__init__.py +0 -0
  27. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/management/commands/bootstrap_field_audit_events.py +0 -0
  28. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/middleware.py +0 -0
  29. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/migrations/0001_initial.py +0 -0
  30. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/migrations/0002_add_is_bootstrap_column.py +0 -0
  31. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/migrations/__init__.py +0 -0
  32. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/field_audit/utils.py +0 -0
  33. {django-field-audit-1.2.8 → django_field_audit-1.3.0}/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.3.0
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
 
@@ -169,6 +177,44 @@ details:
169
177
  are only committed to the database if audit events are successfully created
170
178
  and saved as well.
171
179
 
180
+ ### Auditing Many-to-Many fields
181
+
182
+ Many-to-Many field changes are automatically audited through Django signals when
183
+ included in the `@audit_fields` decorator. Changes to M2M relationships generate
184
+ audit events immediately without requiring `save()` calls.
185
+
186
+ ```python
187
+ # Example model with audited M2M field
188
+ @audit_fields("name", "title", "certifications")
189
+ class CrewMember(models.Model):
190
+ name = models.CharField(max_length=256)
191
+ title = models.CharField(max_length=64)
192
+ certifications = models.ManyToManyField('Certification', blank=True)
193
+ ```
194
+
195
+ #### Supported M2M operations
196
+
197
+ All standard M2M operations create audit events:
198
+
199
+ ```python
200
+ crew_member = CrewMember.objects.create(name='Test Pilot', title='Captain')
201
+ cert1 = Certification.objects.create(name='PPL', certification_type='Private')
202
+
203
+ crew_member.certifications.add(cert1) # Creates audit event
204
+ crew_member.certifications.remove(cert1) # Creates audit event
205
+ crew_member.certifications.set([cert1]) # Creates audit event
206
+ crew_member.certifications.clear() # Creates audit event
207
+ ```
208
+
209
+ #### M2M audit event structure
210
+
211
+ M2M changes use specific delta structures in audit events:
212
+
213
+ - **Add**: `{'certifications': {'add': [1, 2]}}`
214
+ - **Remove**: `{'certifications': {'remove': [2]}}`
215
+ - **Clear**: `{'certifications': {'remove': [1, 2]}}`
216
+ - **Create** / **Bootstrap**: `{'certifications': {'new': []}}`
217
+
172
218
  #### Bootstrap events for models with existing records
173
219
 
174
220
  In the scenario where auditing is enabled for a model with existing data, it can
@@ -1,32 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: django-field-audit
3
- Version: 1.2.8
4
- Summary: Audit Field Changes on Django Models
5
- Home-page: https://github.com/dimagi/django-field-audit
6
- Maintainer: Joel Miller
7
- Maintainer-email: jmiller@dimagi.com
8
- License: BSD License
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Environment :: Web Environment
11
- Classifier: Framework :: Django
12
- Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: BSD License
14
- Classifier: Operating System :: OS Independent
15
- Classifier: Programming Language :: Python
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.9
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Framework :: Django
22
- Classifier: Framework :: Django :: 3
23
- Classifier: Framework :: Django :: 3.2
24
- Classifier: Framework :: Django :: 4
25
- Classifier: Framework :: Django :: 4.2
26
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
- Description-Content-Type: text/markdown
28
- License-File: LICENSE
29
-
30
1
  # Audit Field Changes on Django Models
31
2
 
32
3
  [![tests][tests_badge]][tests_link]
@@ -169,6 +140,44 @@ details:
169
140
  are only committed to the database if audit events are successfully created
170
141
  and saved as well.
171
142
 
143
+ ### Auditing Many-to-Many fields
144
+
145
+ Many-to-Many field changes are automatically audited through Django signals when
146
+ included in the `@audit_fields` decorator. Changes to M2M relationships generate
147
+ audit events immediately without requiring `save()` calls.
148
+
149
+ ```python
150
+ # Example model with audited M2M field
151
+ @audit_fields("name", "title", "certifications")
152
+ class CrewMember(models.Model):
153
+ name = models.CharField(max_length=256)
154
+ title = models.CharField(max_length=64)
155
+ certifications = models.ManyToManyField('Certification', blank=True)
156
+ ```
157
+
158
+ #### Supported M2M operations
159
+
160
+ All standard M2M operations create audit events:
161
+
162
+ ```python
163
+ crew_member = CrewMember.objects.create(name='Test Pilot', title='Captain')
164
+ cert1 = Certification.objects.create(name='PPL', certification_type='Private')
165
+
166
+ crew_member.certifications.add(cert1) # Creates audit event
167
+ crew_member.certifications.remove(cert1) # Creates audit event
168
+ crew_member.certifications.set([cert1]) # Creates audit event
169
+ crew_member.certifications.clear() # Creates audit event
170
+ ```
171
+
172
+ #### M2M audit event structure
173
+
174
+ M2M changes use specific delta structures in audit events:
175
+
176
+ - **Add**: `{'certifications': {'add': [1, 2]}}`
177
+ - **Remove**: `{'certifications': {'remove': [2]}}`
178
+ - **Clear**: `{'certifications': {'remove': [1, 2]}}`
179
+ - **Create** / **Bootstrap**: `{'certifications': {'new': []}}`
180
+
172
181
  #### Bootstrap events for models with existing records
173
182
 
174
183
  In the scenario where auditing is enabled for a model with existing data, it can
@@ -1,3 +1,40 @@
1
+ Metadata-Version: 2.2
2
+ Name: django-field-audit
3
+ Version: 1.3.0
4
+ Summary: Audit Field Changes on Django Models
5
+ Home-page: https://github.com/dimagi/django-field-audit
6
+ Maintainer: Joel Miller
7
+ Maintainer-email: jmiller@dimagi.com
8
+ License: BSD License
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Framework :: Django
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Framework :: Django
22
+ Classifier: Framework :: Django :: 3
23
+ Classifier: Framework :: Django :: 3.2
24
+ Classifier: Framework :: Django :: 4
25
+ Classifier: Framework :: Django :: 4.2
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Description-Content-Type: text/markdown
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
37
+
1
38
  # Audit Field Changes on Django Models
2
39
 
3
40
  [![tests][tests_badge]][tests_link]
@@ -140,6 +177,44 @@ details:
140
177
  are only committed to the database if audit events are successfully created
141
178
  and saved as well.
142
179
 
180
+ ### Auditing Many-to-Many fields
181
+
182
+ Many-to-Many field changes are automatically audited through Django signals when
183
+ included in the `@audit_fields` decorator. Changes to M2M relationships generate
184
+ audit events immediately without requiring `save()` calls.
185
+
186
+ ```python
187
+ # Example model with audited M2M field
188
+ @audit_fields("name", "title", "certifications")
189
+ class CrewMember(models.Model):
190
+ name = models.CharField(max_length=256)
191
+ title = models.CharField(max_length=64)
192
+ certifications = models.ManyToManyField('Certification', blank=True)
193
+ ```
194
+
195
+ #### Supported M2M operations
196
+
197
+ All standard M2M operations create audit events:
198
+
199
+ ```python
200
+ crew_member = CrewMember.objects.create(name='Test Pilot', title='Captain')
201
+ cert1 = Certification.objects.create(name='PPL', certification_type='Private')
202
+
203
+ crew_member.certifications.add(cert1) # Creates audit event
204
+ crew_member.certifications.remove(cert1) # Creates audit event
205
+ crew_member.certifications.set([cert1]) # Creates audit event
206
+ crew_member.certifications.clear() # Creates audit event
207
+ ```
208
+
209
+ #### M2M audit event structure
210
+
211
+ M2M changes use specific delta structures in audit events:
212
+
213
+ - **Add**: `{'certifications': {'add': [1, 2]}}`
214
+ - **Remove**: `{'certifications': {'remove': [2]}}`
215
+ - **Clear**: `{'certifications': {'remove': [1, 2]}}`
216
+ - **Create** / **Bootstrap**: `{'certifications': {'new': []}}`
217
+
143
218
  #### Bootstrap events for models with existing records
144
219
 
145
220
  In the scenario where auditing is enabled for a model with existing data, it can
@@ -19,4 +19,14 @@ 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/0003_alter_auditevent_change_context_and_more.py
23
+ field_audit/migrations/__init__.py
24
+ tests/test_apps.py
25
+ tests/test_auditors.py
26
+ tests/test_bootstrap_field_audit_events.py
27
+ tests/test_django_compat.py
28
+ tests/test_field_audit.py
29
+ tests/test_m2m.py
30
+ tests/test_middleware.py
31
+ tests/test_models.py
32
+ 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.3.0"
@@ -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:
@@ -2,6 +2,7 @@ import contextvars
2
2
  from functools import wraps
3
3
 
4
4
  from django.db import models, router, transaction
5
+ from django.db.models.signals import m2m_changed
5
6
 
6
7
  from .utils import get_fqcn
7
8
 
@@ -63,6 +64,8 @@ def audit_fields(*field_names, class_path=None, audit_special_queryset_writes=Fa
63
64
  cls.save = _decorate_db_write(cls.save)
64
65
  cls.delete = _decorate_db_write(cls.delete)
65
66
  cls.refresh_from_db = _decorate_refresh_from_db(cls.refresh_from_db)
67
+
68
+ _register_m2m_signals(cls, field_names)
66
69
  _audited_models[cls] = get_fqcn(cls) if class_path is None else class_path # noqa: E501
67
70
  return cls
68
71
  if not field_names:
@@ -154,6 +157,86 @@ def _decorate_refresh_from_db(func):
154
157
  return wrapper
155
158
 
156
159
 
160
+ def _register_m2m_signals(cls, field_names):
161
+ """Register m2m_changed signal handlers for ManyToManyFields.
162
+
163
+ :param cls: The model class being audited
164
+ :param field_names: List of field names that are being audited
165
+ """
166
+ for field_name in field_names:
167
+ try:
168
+ field = cls._meta.get_field(field_name)
169
+ if isinstance(field, models.ManyToManyField):
170
+ m2m_changed.connect(
171
+ _m2m_changed_handler,
172
+ sender=field.remote_field.through,
173
+ weak=False
174
+ )
175
+ except Exception:
176
+ # If field doesn't exist or isn't a M2M field, continue
177
+ continue
178
+
179
+
180
+ def _m2m_changed_handler(sender, instance, action, pk_set, **kwargs):
181
+ """Signal handler for m2m_changed to audit ManyToManyField changes.
182
+
183
+ :param sender: The intermediate model class for the ManyToManyField
184
+ :param instance: The instance whose many-to-many relation is updated
185
+ :param action: A string indicating the type of update
186
+ :param pk_set: For add/remove actions, set of primary key values
187
+ """
188
+ from .models import AuditEvent
189
+
190
+ if action not in ('post_add', 'post_remove', 'post_clear', 'pre_clear'):
191
+ return
192
+
193
+ if type(instance) not in _audited_models:
194
+ return
195
+
196
+ # Find which M2M field this change relates to
197
+ m2m_field = None
198
+ field_name = None
199
+ for field in instance._meta.get_fields():
200
+ if (
201
+ isinstance(field, models.ManyToManyField) and
202
+ hasattr(field, 'remote_field') and
203
+ field.remote_field.through == sender
204
+ ):
205
+ m2m_field = field
206
+ field_name = field.name
207
+ break
208
+
209
+ if not m2m_field or field_name not in AuditEvent.field_names(instance):
210
+ return
211
+
212
+ if action == 'pre_clear':
213
+ # `pk_set` not supplied for clear actions. Determine initial values
214
+ # in the `pre_clear` event
215
+ AuditEvent.attach_initial_m2m_values(instance, field_name)
216
+ return
217
+
218
+ if action == 'post_clear':
219
+ initial_values = AuditEvent.get_initial_m2m_values(instance, field_name)
220
+ if not initial_values:
221
+ return
222
+ delta = {field_name: {'remove': initial_values}}
223
+ else:
224
+ if not pk_set:
225
+ # the change was a no-op
226
+ return
227
+ delta_key = 'add' if action == 'post_add' else 'remove'
228
+ delta = {field_name: {delta_key: list(pk_set)}}
229
+
230
+ req = request.get()
231
+ event = AuditEvent.create_audit_event(
232
+ instance.pk, instance.__class__, delta, False, False, req
233
+ )
234
+ if event is not None:
235
+ event.save()
236
+
237
+ AuditEvent.clear_initial_m2m_field_values(instance, field_name)
238
+
239
+
157
240
  def get_audited_models():
158
241
  return _audited_models.copy()
159
242
 
@@ -0,0 +1,32 @@
1
+ # Generated by Django 5.1.6 on 2025-06-27 15:11
2
+
3
+ import django.core.serializers.json
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('field_audit', '0002_add_is_bootstrap_column'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='auditevent',
16
+ name='change_context',
17
+ field=models.JSONField(
18
+ encoder=django.core.serializers.json.DjangoJSONEncoder),
19
+ ),
20
+ migrations.AlterField(
21
+ model_name='auditevent',
22
+ name='delta',
23
+ field=models.JSONField(
24
+ encoder=django.core.serializers.json.DjangoJSONEncoder),
25
+ ),
26
+ migrations.AlterField(
27
+ model_name='auditevent',
28
+ name='object_pk',
29
+ field=models.JSONField(
30
+ encoder=django.core.serializers.json.DjangoJSONEncoder),
31
+ ),
32
+ ]
@@ -3,6 +3,7 @@ from functools import wraps
3
3
  from itertools import islice
4
4
 
5
5
  from django.conf import settings
6
+ from django.core.serializers.json import DjangoJSONEncoder
6
7
  from django.db import models, transaction
7
8
  from django.db.models import Expression
8
9
  from django.utils import timezone
@@ -203,12 +204,12 @@ def get_date():
203
204
  class AuditEvent(models.Model):
204
205
  event_date = models.DateTimeField(default=get_date, db_index=True)
205
206
  object_class_path = models.CharField(db_index=True, max_length=255)
206
- object_pk = models.JSONField()
207
- change_context = models.JSONField()
207
+ object_pk = models.JSONField(encoder=DjangoJSONEncoder)
208
+ change_context = models.JSONField(encoder=DjangoJSONEncoder)
208
209
  is_create = models.BooleanField(default=False)
209
210
  is_delete = models.BooleanField(default=False)
210
211
  is_bootstrap = models.BooleanField(default=False)
211
- delta = models.JSONField()
212
+ delta = models.JSONField(encoder=DjangoJSONEncoder)
212
213
 
213
214
  objects = get_manager("AUDITEVENT_MANAGER", DefaultAuditEventManager)
214
215
 
@@ -226,6 +227,7 @@ class AuditEvent(models.Model):
226
227
 
227
228
  ATTACH_FIELD_NAMES_AT = "__field_audit_field_names"
228
229
  ATTACH_INIT_VALUES_AT = "__field_audit_init_values"
230
+ ATTACH_INIT_M2M_VALUES_AT = "__field_audit_init_m2m_values"
229
231
 
230
232
  @classmethod
231
233
  def attach_field_names(cls, model_class, field_names):
@@ -245,13 +247,19 @@ class AuditEvent(models.Model):
245
247
  return getattr(model_class, cls.ATTACH_FIELD_NAMES_AT)
246
248
 
247
249
  @staticmethod
248
- def get_field_value(instance, field_name):
250
+ def get_field_value(instance, field_name, bootstrap=False):
249
251
  """Returns the database value of a field on ``instance``.
250
252
 
251
253
  :param instance: an instance of a Django model
252
254
  :param field_name: name of a field on ``instance``
253
255
  """
254
256
  field = instance._meta.get_field(field_name)
257
+
258
+ if isinstance(field, models.ManyToManyField):
259
+ # ManyToManyField handled by Django signals
260
+ if bootstrap:
261
+ return AuditEvent.get_m2m_field_value(instance, field_name)
262
+ return []
255
263
  return field.to_python(field.value_from_object(instance))
256
264
 
257
265
  @classmethod
@@ -275,6 +283,44 @@ class AuditEvent(models.Model):
275
283
  init_values = {f: cls.get_field_value(instance, f) for f in field_names}
276
284
  setattr(instance, cls.ATTACH_INIT_VALUES_AT, init_values)
277
285
 
286
+ @classmethod
287
+ def attach_initial_m2m_values(cls, instance, field_name):
288
+ field = instance._meta.get_field(field_name)
289
+ if not isinstance(field, models.ManyToManyField):
290
+ return None
291
+
292
+ values = cls.get_m2m_field_value(instance, field_name)
293
+ init_values = getattr(
294
+ instance, cls.ATTACH_INIT_M2M_VALUES_AT, None
295
+ ) or {}
296
+ init_values.update({field_name: values})
297
+ setattr(instance, cls.ATTACH_INIT_M2M_VALUES_AT, init_values)
298
+
299
+ @classmethod
300
+ def get_initial_m2m_values(cls, instance, field_name):
301
+ init_values = getattr(
302
+ instance, cls.ATTACH_INIT_M2M_VALUES_AT, None
303
+ ) or {}
304
+ return init_values.get(field_name)
305
+
306
+ @classmethod
307
+ def clear_initial_m2m_field_values(cls, instance, field_name):
308
+ init_values = getattr(
309
+ instance, cls.ATTACH_INIT_M2M_VALUES_AT, None
310
+ ) or {}
311
+ init_values.pop(field_name, None)
312
+ setattr(instance, cls.ATTACH_INIT_M2M_VALUES_AT, init_values)
313
+
314
+ @classmethod
315
+ def get_m2m_field_value(cls, instance, field_name):
316
+ if instance.pk is None:
317
+ # Instance is not saved, return empty list
318
+ return []
319
+ else:
320
+ # Instance is saved, we can access the related objects
321
+ related_manager = getattr(instance, field_name)
322
+ return list(related_manager.values_list('pk', flat=True))
323
+
278
324
  @classmethod
279
325
  def reset_initial_values(cls, instance):
280
326
  """Returns the previously attached "initial values" and attaches new
@@ -396,7 +442,6 @@ class AuditEvent(models.Model):
396
442
  object_pk = instance.pk
397
443
 
398
444
  delta = cls.get_delta_from_instance(instance, is_create, is_delete)
399
-
400
445
  if delta:
401
446
  return cls.create_audit_event(object_pk, type(instance), delta,
402
447
  is_create, is_delete, request)
@@ -471,7 +516,9 @@ class AuditEvent(models.Model):
471
516
  for instance in iter_records():
472
517
  delta = {}
473
518
  for field_name in field_names:
474
- value = cls.get_field_value(instance, field_name)
519
+ value = cls.get_field_value(
520
+ instance, field_name, bootstrap=True
521
+ )
475
522
  delta[field_name] = {"new": value}
476
523
  yield cls(
477
524
  object_class_path=object_class_path,
@@ -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()