django-field-audit 1.2.4__tar.gz → 1.2.5__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 (23) hide show
  1. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/PKG-INFO +11 -10
  2. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/README.md +10 -9
  3. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/django_field_audit.egg-info/PKG-INFO +11 -10
  4. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/__init__.py +1 -1
  5. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/field_audit.py +13 -10
  6. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/management/commands/bootstrap_field_audit_events.py +1 -9
  7. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/models.py +182 -79
  8. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/LICENSE +0 -0
  9. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/django_field_audit.egg-info/SOURCES.txt +0 -0
  10. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/django_field_audit.egg-info/dependency_links.txt +0 -0
  11. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/django_field_audit.egg-info/top_level.txt +0 -0
  12. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/apps.py +0 -0
  13. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/auditors.py +0 -0
  14. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/const.py +0 -0
  15. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/management/__init__.py +0 -0
  16. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/management/commands/__init__.py +0 -0
  17. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/middleware.py +0 -0
  18. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/migrations/0001_initial.py +0 -0
  19. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/migrations/0002_add_is_bootstrap_column.py +0 -0
  20. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/migrations/__init__.py +0 -0
  21. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/utils.py +0 -0
  22. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/setup.cfg +0 -0
  23. {django-field-audit-1.2.4 → django-field-audit-1.2.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-field-audit
3
- Version: 1.2.4
3
+ Version: 1.2.5
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
@@ -151,13 +151,16 @@ details:
151
151
  - Specifying `audit_special_queryset_writes=True` (step **1** above) without
152
152
  setting the default manager to an instance of `AuditingManager` (step **2**
153
153
  above) will raise an exception when the model class is evaluated.
154
- - At this time, only the `QuerySet.delete()` and `QuerySet.update()` "special"
155
- write methods can actually perform change auditing when called with
156
- `audit_action=AuditAction.AUDIT`. The other two methods are currently not
157
- implemented and will raise `NotImplementedError` if called with that action.
158
- Implementing these remaining methods remains a task for the future, see
159
- **TODO** below. All four methods do support `audit_action=AuditAction.IGNORE`
160
- usage, however.
154
+ - At this time, `QuerySet.delete()`, `QuerySet.update()`,
155
+ and `QuerySet.bulk_create()` "special" write methods can actually perform
156
+ change auditing when called with `audit_action=AuditAction.AUDIT`.
157
+ `QuerySet.bulk_update()` is not currently implemented and will raise
158
+ `NotImplementedError` if called with that action. Implementing this remaining
159
+ method remains a task for the future, see **TODO** below. All four methods do
160
+ support `audit_action=AuditAction.IGNORE` usage, however.
161
+ - All audited methods use transactions to ensure changes to audited models
162
+ are only committed to the database if audit events are successfully created
163
+ and saved as well.
161
164
 
162
165
  #### Bootstrap events for models with existing records
163
166
 
@@ -276,11 +279,9 @@ twine upload dist/*
276
279
  ## TODO
277
280
 
278
281
  - Implement auditing for the remaining "special" QuerySet write operations:
279
- - `bulk_create()`
280
282
  - `bulk_update()`
281
283
  - Write full library documentation using github.io.
282
284
  - Switch to `pytest` to support Python 3.10.
283
- - Wrap audited DB write methods in Django's transaction.atomic context manager
284
285
 
285
286
  ### Backlog
286
287
 
@@ -124,13 +124,16 @@ details:
124
124
  - Specifying `audit_special_queryset_writes=True` (step **1** above) without
125
125
  setting the default manager to an instance of `AuditingManager` (step **2**
126
126
  above) will raise an exception when the model class is evaluated.
127
- - At this time, only the `QuerySet.delete()` and `QuerySet.update()` "special"
128
- write methods can actually perform change auditing when called with
129
- `audit_action=AuditAction.AUDIT`. The other two methods are currently not
130
- implemented and will raise `NotImplementedError` if called with that action.
131
- Implementing these remaining methods remains a task for the future, see
132
- **TODO** below. All four methods do support `audit_action=AuditAction.IGNORE`
133
- usage, however.
127
+ - At this time, `QuerySet.delete()`, `QuerySet.update()`,
128
+ and `QuerySet.bulk_create()` "special" write methods can actually perform
129
+ change auditing when called with `audit_action=AuditAction.AUDIT`.
130
+ `QuerySet.bulk_update()` is not currently implemented and will raise
131
+ `NotImplementedError` if called with that action. Implementing this remaining
132
+ method remains a task for the future, see **TODO** below. All four methods do
133
+ support `audit_action=AuditAction.IGNORE` usage, however.
134
+ - All audited methods use transactions to ensure changes to audited models
135
+ are only committed to the database if audit events are successfully created
136
+ and saved as well.
134
137
 
135
138
  #### Bootstrap events for models with existing records
136
139
 
@@ -249,11 +252,9 @@ twine upload dist/*
249
252
  ## TODO
250
253
 
251
254
  - Implement auditing for the remaining "special" QuerySet write operations:
252
- - `bulk_create()`
253
255
  - `bulk_update()`
254
256
  - Write full library documentation using github.io.
255
257
  - Switch to `pytest` to support Python 3.10.
256
- - Wrap audited DB write methods in Django's transaction.atomic context manager
257
258
 
258
259
  ### Backlog
259
260
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-field-audit
3
- Version: 1.2.4
3
+ Version: 1.2.5
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
@@ -151,13 +151,16 @@ details:
151
151
  - Specifying `audit_special_queryset_writes=True` (step **1** above) without
152
152
  setting the default manager to an instance of `AuditingManager` (step **2**
153
153
  above) will raise an exception when the model class is evaluated.
154
- - At this time, only the `QuerySet.delete()` and `QuerySet.update()` "special"
155
- write methods can actually perform change auditing when called with
156
- `audit_action=AuditAction.AUDIT`. The other two methods are currently not
157
- implemented and will raise `NotImplementedError` if called with that action.
158
- Implementing these remaining methods remains a task for the future, see
159
- **TODO** below. All four methods do support `audit_action=AuditAction.IGNORE`
160
- usage, however.
154
+ - At this time, `QuerySet.delete()`, `QuerySet.update()`,
155
+ and `QuerySet.bulk_create()` "special" write methods can actually perform
156
+ change auditing when called with `audit_action=AuditAction.AUDIT`.
157
+ `QuerySet.bulk_update()` is not currently implemented and will raise
158
+ `NotImplementedError` if called with that action. Implementing this remaining
159
+ method remains a task for the future, see **TODO** below. All four methods do
160
+ support `audit_action=AuditAction.IGNORE` usage, however.
161
+ - All audited methods use transactions to ensure changes to audited models
162
+ are only committed to the database if audit events are successfully created
163
+ and saved as well.
161
164
 
162
165
  #### Bootstrap events for models with existing records
163
166
 
@@ -276,11 +279,9 @@ twine upload dist/*
276
279
  ## TODO
277
280
 
278
281
  - Implement auditing for the remaining "special" QuerySet write operations:
279
- - `bulk_create()`
280
282
  - `bulk_update()`
281
283
  - Write full library documentation using github.io.
282
284
  - Switch to `pytest` to support Python 3.10.
283
- - Wrap audited DB write methods in Django's transaction.atomic context manager
284
285
 
285
286
  ### Backlog
286
287
 
@@ -1,3 +1,3 @@
1
1
  from .field_audit import audit_fields # noqa: F401
2
2
 
3
- __version__ = "1.2.4"
3
+ __version__ = "1.2.5"
@@ -1,7 +1,7 @@
1
1
  import contextvars
2
2
  from functools import wraps
3
3
 
4
- from django.db import models
4
+ from django.db import models, router, transaction
5
5
 
6
6
  from .utils import get_fqcn
7
7
 
@@ -116,15 +116,18 @@ def _decorate_db_write(func):
116
116
  # - https://stackoverflow.com/questions/907695/
117
117
  is_create = is_save and self._state.adding
118
118
  object_pk = self.pk if is_delete else None
119
- ret = func(self, *args, **kw)
120
- AuditEvent.audit_field_changes(
121
- self,
122
- is_create,
123
- is_delete,
124
- request.get(),
125
- object_pk,
126
- )
127
- return ret
119
+
120
+ db = router.db_for_write(type(self))
121
+ with transaction.atomic(using=db):
122
+ ret = func(self, *args, **kw)
123
+ AuditEvent.audit_field_changes(
124
+ self,
125
+ is_create,
126
+ is_delete,
127
+ request.get(),
128
+ object_pk,
129
+ )
130
+ return ret
128
131
  is_save = func.__name__ == "save"
129
132
  is_delete = func.__name__ == "delete"
130
133
  if not is_save and not is_delete:
@@ -86,7 +86,7 @@ class Command(BaseCommand):
86
86
  stream.write(f"done ({count})")
87
87
 
88
88
  def do_bootstrap(self, model_class, bootstrap_method, **bootstrap_kw):
89
- field_names = self.get_field_names(model_class)
89
+ field_names = AuditEvent.field_names(model_class)
90
90
  if not field_names:
91
91
  raise CommandError(
92
92
  f"invalid fields ({field_names!r}) for model: {model_class}"
@@ -103,14 +103,6 @@ class Command(BaseCommand):
103
103
  "top-up": top_up_missing,
104
104
  }
105
105
 
106
- @staticmethod
107
- def get_field_names(model_class):
108
- """Extract the field names from a model class.
109
-
110
- TODO: expose a method on the AuditEvent class for doing this.
111
- """
112
- return AuditEvent._field_names(model_class())
113
-
114
106
  @contextmanager
115
107
  def bootstrap_action_log(self, *args, **kw):
116
108
  end = kw.pop("end", "\n")
@@ -4,7 +4,7 @@ from functools import wraps
4
4
  from itertools import islice
5
5
 
6
6
  from django.conf import settings
7
- from django.db import models, router, transaction
7
+ from django.db import models, transaction
8
8
 
9
9
  from .const import BOOTSTRAP_BATCH_SIZE
10
10
  from .utils import class_import_helper
@@ -236,12 +236,12 @@ class AuditEvent(models.Model):
236
236
  setattr(model_class, cls.ATTACH_FIELD_NAMES_AT, field_names)
237
237
 
238
238
  @classmethod
239
- def _field_names(cls, instance):
240
- """Returns the audit field names stored on the model instance's class
239
+ def field_names(cls, model_class):
240
+ """Returns the audit field names stored on the audited Model class
241
241
 
242
- :param instance: instance of a Model subclass being audited for changes
242
+ :param model_class: a Django Model class under audit
243
243
  """
244
- return getattr(instance.__class__, cls.ATTACH_FIELD_NAMES_AT)
244
+ return getattr(model_class, cls.ATTACH_FIELD_NAMES_AT)
245
245
 
246
246
  @staticmethod
247
247
  def get_field_value(instance, field_name):
@@ -270,7 +270,7 @@ class AuditEvent(models.Model):
270
270
  f"refusing to overwrite {cls.ATTACH_INIT_VALUES_AT!r} "
271
271
  f"on model instance: {instance}"
272
272
  )
273
- field_names = cls._field_names(instance)
273
+ field_names = cls.field_names(instance)
274
274
  init_values = {f: cls.get_field_value(instance, f) for f in field_names}
275
275
  setattr(instance, cls.ATTACH_INIT_VALUES_AT, init_values)
276
276
 
@@ -293,19 +293,79 @@ class AuditEvent(models.Model):
293
293
 
294
294
  @classmethod
295
295
  def audit_field_changes(cls, *args, **kw):
296
- """Convenience method that calls ``make_audit_event()`` and saves the
297
- event (if one is returned).
296
+ """Convenience method that calls ``make_audit_event_from_instance()``
297
+ and saves the event (if one is returned).
298
298
 
299
- All [keyword] arguments are passed directly to ``make_audit_event()``,
300
- see that method for usage.
299
+ All [keyword] arguments are passed directly to
300
+ ``make_audit_event_from_instance()``, see that method for usage.
301
301
  """
302
- event = cls.make_audit_event(*args, **kw)
302
+ event = cls.make_audit_event_from_instance(*args, **kw)
303
303
  if event is not None:
304
304
  event.save()
305
305
 
306
306
  @classmethod
307
- def make_audit_event(cls, instance, is_create, is_delete,
308
- request, object_pk=None, init_values=None):
307
+ def get_delta_from_instance(cls, instance, is_create, is_delete):
308
+ """
309
+ Returns a dictionary representing the delta of an instance of a model
310
+ being audited for changes.
311
+
312
+ :param instance: instance of a Model subclass to be audited for changes
313
+ :param is_create: whether or not the audited event creates a new DB
314
+ record (setting ``True`` implies that ``instance`` is changing)
315
+ :param is_delete: whether or not the audited event deletes an existing
316
+ DB record (setting ``True`` implies that ``instance`` is changing)
317
+ :returns: {field_name: {'old': old_value, 'new': new_value}, ...}
318
+ :raises: ``AssertionError`` if both is_create and is_delete are true
319
+ """
320
+ assert not (is_create and is_delete),\
321
+ "is_create and is_delete cannot both be true"
322
+ fields_to_audit = cls.field_names(instance)
323
+ # fetch (and reset for next db write operation) initial values
324
+ old_values = {} if is_create else cls.reset_initial_values(instance)
325
+ new_values = {} if is_delete else \
326
+ {f: cls.get_field_value(instance, f) for f in fields_to_audit}
327
+ return cls.create_delta(old_values, new_values)
328
+
329
+ @staticmethod
330
+ def create_delta(old_values, new_values):
331
+ """
332
+ Compares two dictionaries and creates a delta between the two
333
+
334
+ :param old_values: {field_name: field_value, ...} representing the
335
+ values prior to a change
336
+ :param new_values: {field_name: field_value, ...} representing the
337
+ values after a change
338
+ :returns: {field_name: {'old': old_value, 'new': new_value}, ...}
339
+ :raises: ``AssertionError`` if both old_values and new_values are empty
340
+ do not match
341
+ """
342
+ assert old_values or new_values, \
343
+ "Must provide a non-empty value for either old_values or new_values"
344
+
345
+ changed_fields = old_values.keys() if old_values else new_values.keys()
346
+ if old_values and new_values:
347
+ changed_fields = new_values.keys()
348
+
349
+ delta = {}
350
+ for field_name in changed_fields:
351
+ if not old_values:
352
+ delta[field_name] = {"new": new_values[field_name]}
353
+ elif not new_values:
354
+ delta[field_name] = {"old": old_values[field_name]}
355
+ else:
356
+ try:
357
+ old_value = old_values[field_name]
358
+ except KeyError:
359
+ delta[field_name] = {"new": new_values[field_name]}
360
+ else:
361
+ if old_value != new_values[field_name]:
362
+ delta[field_name] = {"old": old_value,
363
+ "new": new_values[field_name]}
364
+ return delta
365
+
366
+ @classmethod
367
+ def make_audit_event_from_instance(cls, instance, is_create, is_delete,
368
+ request, object_pk=None):
309
369
  """Factory method for creating a new ``AuditEvent`` for an instance of a
310
370
  model that's being audited for changes.
311
371
 
@@ -320,9 +380,6 @@ class AuditEvent(models.Model):
320
380
  ``is_delete == True``, that is, when the instance itself no longer
321
381
  references its pre-delete primary key. It is ambiguous to set this
322
382
  when ``is_delete == False``, and doing so will raise an exception.
323
- :param init_values: (Optional) dictionary of initial values for audited
324
- fields on the provided instance. If not provided, will use
325
- AuditEvent.ATTACH_INIT_VALUES_AT attribute.
326
383
  :returns: an unsaved ``AuditEvent`` instance (or ``None`` if
327
384
  ``instance`` has not changed)
328
385
  :raises: ``ValueError`` on invalid use of the ``object_pk`` argument
@@ -333,37 +390,52 @@ class AuditEvent(models.Model):
333
390
  "'object_pk' arg is ambiguous when 'is_delete == False'"
334
391
  )
335
392
  object_pk = instance.pk
336
- # fetch (and reset for next db write operation) initial values
337
- fields_to_audit = init_values.keys() if init_values else \
338
- cls._field_names(instance)
339
- init_values = init_values or cls.reset_initial_values(instance)
340
- delta = {}
341
- for field_name in fields_to_audit:
342
- value = cls.get_field_value(instance, field_name)
343
- if is_create:
344
- delta[field_name] = {"new": value}
345
- elif is_delete:
346
- delta[field_name] = {"old": value}
347
- else:
348
- try:
349
- init_value = init_values[field_name]
350
- except KeyError:
351
- delta[field_name] = {"new": value}
352
- else:
353
- if init_value != value:
354
- delta[field_name] = {"old": init_value, "new": value}
393
+
394
+ delta = cls.get_delta_from_instance(instance, is_create, is_delete)
395
+
355
396
  if delta:
356
- from .auditors import audit_dispatcher
357
- from .field_audit import get_audited_class_path
358
- change_context = audit_dispatcher.dispatch(request)
359
- return cls(
360
- object_class_path=get_audited_class_path(type(instance)),
361
- object_pk=object_pk,
362
- change_context=cls._change_context_db_value(change_context),
363
- is_create=is_create,
364
- is_delete=is_delete,
365
- delta=delta,
366
- )
397
+ return cls.create_audit_event(object_pk, type(instance), delta,
398
+ is_create, is_delete, request)
399
+
400
+ @classmethod
401
+ def make_audit_event_from_values(cls, old_values, new_values, object_pk,
402
+ object_cls, request):
403
+ """Factory method for creating a new ``AuditEvent`` based on old and new
404
+ values.
405
+
406
+ :param old_values: {field_name: field_value, ...} representing the
407
+ values prior to a change
408
+ :param new_values: {field_name: field_value, ...} representing the
409
+ values after a change
410
+ :param object_pk: primary key of the instance
411
+ :param object_cls: class type of the object being audited
412
+ :param request: the request object responsible for the change (or
413
+ ``None`` if there is no request)
414
+ :returns: an unsaved ``AuditEvent`` instance (or ``None`` if
415
+ no difference between ``old_values`` and ``new_values``)
416
+ """
417
+ is_create = not old_values
418
+ is_delete = not new_values
419
+ delta = AuditEvent.create_delta(old_values, new_values)
420
+ if delta:
421
+ return AuditEvent.create_audit_event(object_pk, object_cls, delta,
422
+ is_create, is_delete, request)
423
+
424
+ @classmethod
425
+ def create_audit_event(cls, object_pk, object_cls, delta, is_create,
426
+ is_delete, request):
427
+ from .auditors import audit_dispatcher
428
+ from .field_audit import get_audited_class_path
429
+ change_context = audit_dispatcher.dispatch(request)
430
+ object_cls_path = get_audited_class_path(object_cls)
431
+ return cls(
432
+ object_class_path=object_cls_path,
433
+ object_pk=object_pk,
434
+ change_context=cls._change_context_db_value(change_context),
435
+ is_create=is_create,
436
+ is_delete=is_delete,
437
+ delta=delta,
438
+ )
367
439
 
368
440
  @classmethod
369
441
  def bootstrap_existing_model_records(cls, model_class, field_names,
@@ -490,7 +562,14 @@ def validate_audit_action(func):
490
562
  :raises: ``InvalidAuditActionError`` or ``UnsetAuditActionError``
491
563
  """
492
564
  @wraps(func)
493
- def wrapper(self, *args, audit_action=AuditAction.RAISE, **kw):
565
+ def wrapper(self, *args, **kw):
566
+ try:
567
+ audit_action = kw["audit_action"]
568
+ except KeyError:
569
+ raise UnsetAuditActionError(
570
+ f"{type(self).__name__}.{func.__name__}() requires an audit "
571
+ "action as a keyword argument."
572
+ )
494
573
  if audit_action not in AuditAction:
495
574
  raise InvalidAuditActionError(
496
575
  "The 'audit_action' argument must be a value of 'AuditAction', "
@@ -501,7 +580,7 @@ def validate_audit_action(func):
501
580
  f"{type(self).__name__}.{func.__name__}() requires an audit "
502
581
  "action"
503
582
  )
504
- return func(self, *args, audit_action=audit_action, **kw)
583
+ return func(self, *args, **kw)
505
584
  return wrapper
506
585
 
507
586
 
@@ -532,13 +611,23 @@ class AuditingQuerySet(models.QuerySet):
532
611
  """
533
612
 
534
613
  @validate_audit_action
535
- def bulk_create(self, *args, audit_action=AuditAction.RAISE, **kw):
536
- if audit_action is AuditAction.IGNORE:
537
- return super().bulk_create(*args, **kw)
538
- else:
539
- raise NotImplementedError(
540
- "Change auditing is not implemented for bulk_create()."
541
- )
614
+ def bulk_create(self, objs, *, audit_action=AuditAction.RAISE, **kw):
615
+ if audit_action is AuditAction.IGNORE or not objs:
616
+ return super().bulk_create(objs, **kw)
617
+ assert audit_action is AuditAction.AUDIT, audit_action
618
+
619
+ from .field_audit import request
620
+ request = request.get()
621
+
622
+ with transaction.atomic(using=self.db):
623
+ created_objs = super().bulk_create(objs, **kw)
624
+ audit_events = []
625
+ for obj in created_objs:
626
+ audit_events.append(
627
+ AuditEvent.make_audit_event_from_instance(
628
+ obj, True, False, request))
629
+ AuditEvent.objects.bulk_create(audit_events)
630
+ return created_objs
542
631
 
543
632
  @validate_audit_action
544
633
  def bulk_update(self, *args, audit_action=AuditAction.RAISE, **kw):
@@ -557,52 +646,66 @@ class AuditingQuerySet(models.QuerySet):
557
646
  from .field_audit import request
558
647
  request = request.get()
559
648
  audit_events = []
560
- for instance in self:
561
- # make_audit_event() will never return None because delete=True
562
- audit_events.append(AuditEvent.make_audit_event(
563
- instance,
564
- False,
565
- True,
566
- request,
567
- instance.pk,
568
- ))
569
- value = super().delete()
570
- if audit_events:
571
- # write the audit events _after_ the delete succeeds
572
- AuditEvent.objects.bulk_create(audit_events)
573
- return value
649
+ fields_to_fetch = set(AuditEvent.field_names(self.model)) | {'pk'}
650
+ current_values = {}
651
+ for values_for_instance in self.values(*fields_to_fetch):
652
+ pk = values_for_instance.pop('pk')
653
+ current_values[pk] = values_for_instance
654
+
655
+ for pk, current_values_for_pk in current_values.items():
656
+ audit_event = AuditEvent.make_audit_event_from_values(
657
+ current_values_for_pk,
658
+ {},
659
+ pk,
660
+ self.model,
661
+ request
662
+ )
663
+ audit_events.append(audit_event)
664
+
665
+ with transaction.atomic(using=self.db):
666
+ value = super().delete()
667
+ if audit_events:
668
+ # write the audit events _after_ the delete succeeds
669
+ AuditEvent.objects.bulk_create(audit_events)
670
+ return value
574
671
 
575
672
  @validate_audit_action
576
- def update(self, audit_action=AuditAction.RAISE, **kw):
673
+ def update(self, *, audit_action=AuditAction.RAISE, **kw):
674
+ """
675
+ In order to determine the old and new values of the records matched by
676
+ the queryset, a fetch of audited values for the matched records is
677
+ performed, resulting in one fetch of the current values, one update of
678
+ the matched records, and one bulk creation of audit events.
679
+ """
577
680
  if audit_action is AuditAction.IGNORE:
578
681
  return super().update(**kw)
579
682
  assert audit_action is AuditAction.AUDIT, audit_action
580
683
 
581
684
  fields_to_update = set(kw.keys())
582
- audited_fields = set(
583
- getattr(self.model, AuditEvent.ATTACH_FIELD_NAMES_AT)
584
- )
685
+ audited_fields = set(AuditEvent.field_names(self.model))
585
686
  fields_to_audit = fields_to_update & audited_fields
586
687
  if not fields_to_audit:
587
688
  # no audited fields are changing
588
689
  return super().update(**kw)
589
690
 
590
- values_to_fetch = fields_to_update | {"pk"}
691
+ new_values = {field: kw[field] for field in fields_to_audit}
692
+
591
693
  old_values = {}
694
+ values_to_fetch = fields_to_update | {"pk"}
592
695
  for value in self.values(*values_to_fetch):
593
696
  pk = value.pop('pk')
594
697
  old_values[pk] = value
595
698
 
596
- with transaction.atomic(using=router.db_for_write(self.model)):
699
+ with transaction.atomic(using=self.db):
597
700
  rows = super().update(**kw)
598
701
  # create and write the audit events _after_ the update succeeds
599
702
  from .field_audit import request
600
703
  request = request.get()
601
704
  audit_events = []
602
- for instance in self:
603
- init_values = old_values[instance.pk]
604
- audit_event = AuditEvent.make_audit_event(
605
- instance, False, False, request, init_values=init_values
705
+
706
+ for pk, old_values_for_pk in old_values.items():
707
+ audit_event = AuditEvent.make_audit_event_from_values(
708
+ old_values_for_pk, new_values, pk, self.model, request
606
709
  )
607
710
  if audit_event:
608
711
  audit_events.append(audit_event)