django-field-audit 1.2.3__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.
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/PKG-INFO +10 -8
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/README.md +9 -7
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/django_field_audit.egg-info/PKG-INFO +10 -8
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/__init__.py +1 -1
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/field_audit.py +13 -10
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/management/commands/bootstrap_field_audit_events.py +1 -9
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/models.py +207 -70
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/LICENSE +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/django_field_audit.egg-info/SOURCES.txt +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/django_field_audit.egg-info/dependency_links.txt +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/django_field_audit.egg-info/top_level.txt +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/apps.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/auditors.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/const.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/management/__init__.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/management/commands/__init__.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/middleware.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/migrations/0001_initial.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/migrations/0002_add_is_bootstrap_column.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/migrations/__init__.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/utils.py +0 -0
- {django-field-audit-1.2.3 → django-field-audit-1.2.5}/setup.cfg +0 -0
- {django-field-audit-1.2.3 → 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.
|
|
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,12 +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,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
|
|
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
|
|
159
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.
|
|
160
164
|
|
|
161
165
|
#### Bootstrap events for models with existing records
|
|
162
166
|
|
|
@@ -275,9 +279,7 @@ twine upload dist/*
|
|
|
275
279
|
## TODO
|
|
276
280
|
|
|
277
281
|
- Implement auditing for the remaining "special" QuerySet write operations:
|
|
278
|
-
- `bulk_create()`
|
|
279
282
|
- `bulk_update()`
|
|
280
|
-
- `update()`
|
|
281
283
|
- Write full library documentation using github.io.
|
|
282
284
|
- Switch to `pytest` to support Python 3.10.
|
|
283
285
|
|
|
@@ -124,12 +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,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`
|
|
131
|
-
|
|
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
|
|
132
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.
|
|
133
137
|
|
|
134
138
|
#### Bootstrap events for models with existing records
|
|
135
139
|
|
|
@@ -248,9 +252,7 @@ twine upload dist/*
|
|
|
248
252
|
## TODO
|
|
249
253
|
|
|
250
254
|
- Implement auditing for the remaining "special" QuerySet write operations:
|
|
251
|
-
- `bulk_create()`
|
|
252
255
|
- `bulk_update()`
|
|
253
|
-
- `update()`
|
|
254
256
|
- Write full library documentation using github.io.
|
|
255
257
|
- Switch to `pytest` to support Python 3.10.
|
|
256
258
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-field-audit
|
|
3
|
-
Version: 1.2.
|
|
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,12 +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,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
|
|
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
|
|
159
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.
|
|
160
164
|
|
|
161
165
|
#### Bootstrap events for models with existing records
|
|
162
166
|
|
|
@@ -275,9 +279,7 @@ twine upload dist/*
|
|
|
275
279
|
## TODO
|
|
276
280
|
|
|
277
281
|
- Implement auditing for the remaining "special" QuerySet write operations:
|
|
278
|
-
- `bulk_create()`
|
|
279
282
|
- `bulk_update()`
|
|
280
|
-
- `update()`
|
|
281
283
|
- Write full library documentation using github.io.
|
|
282
284
|
- Switch to `pytest` to support Python 3.10.
|
|
283
285
|
|
|
@@ -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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 =
|
|
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
|
|
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
|
|
240
|
-
"""Returns the audit field names stored on the
|
|
239
|
+
def field_names(cls, model_class):
|
|
240
|
+
"""Returns the audit field names stored on the audited Model class
|
|
241
241
|
|
|
242
|
-
:param
|
|
242
|
+
:param model_class: a Django Model class under audit
|
|
243
243
|
"""
|
|
244
|
-
return getattr(
|
|
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.
|
|
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 ``
|
|
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
|
|
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.
|
|
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
|
|
308
|
-
|
|
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
|
|
|
@@ -330,35 +390,52 @@ class AuditEvent(models.Model):
|
|
|
330
390
|
"'object_pk' arg is ambiguous when 'is_delete == False'"
|
|
331
391
|
)
|
|
332
392
|
object_pk = instance.pk
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
for field_name in cls._field_names(instance):
|
|
337
|
-
value = cls.get_field_value(instance, field_name)
|
|
338
|
-
if is_create:
|
|
339
|
-
delta[field_name] = {"new": value}
|
|
340
|
-
elif is_delete:
|
|
341
|
-
delta[field_name] = {"old": value}
|
|
342
|
-
else:
|
|
343
|
-
try:
|
|
344
|
-
init_value = init_values[field_name]
|
|
345
|
-
except KeyError:
|
|
346
|
-
delta[field_name] = {"new": value}
|
|
347
|
-
else:
|
|
348
|
-
if init_value != value:
|
|
349
|
-
delta[field_name] = {"old": init_value, "new": value}
|
|
393
|
+
|
|
394
|
+
delta = cls.get_delta_from_instance(instance, is_create, is_delete)
|
|
395
|
+
|
|
350
396
|
if delta:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
)
|
|
362
439
|
|
|
363
440
|
@classmethod
|
|
364
441
|
def bootstrap_existing_model_records(cls, model_class, field_names,
|
|
@@ -485,7 +562,14 @@ def validate_audit_action(func):
|
|
|
485
562
|
:raises: ``InvalidAuditActionError`` or ``UnsetAuditActionError``
|
|
486
563
|
"""
|
|
487
564
|
@wraps(func)
|
|
488
|
-
def wrapper(self, *args,
|
|
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
|
+
)
|
|
489
573
|
if audit_action not in AuditAction:
|
|
490
574
|
raise InvalidAuditActionError(
|
|
491
575
|
"The 'audit_action' argument must be a value of 'AuditAction', "
|
|
@@ -496,7 +580,7 @@ def validate_audit_action(func):
|
|
|
496
580
|
f"{type(self).__name__}.{func.__name__}() requires an audit "
|
|
497
581
|
"action"
|
|
498
582
|
)
|
|
499
|
-
return func(self, *args,
|
|
583
|
+
return func(self, *args, **kw)
|
|
500
584
|
return wrapper
|
|
501
585
|
|
|
502
586
|
|
|
@@ -527,13 +611,23 @@ class AuditingQuerySet(models.QuerySet):
|
|
|
527
611
|
"""
|
|
528
612
|
|
|
529
613
|
@validate_audit_action
|
|
530
|
-
def bulk_create(self,
|
|
531
|
-
if audit_action is AuditAction.IGNORE:
|
|
532
|
-
return super().bulk_create(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
537
631
|
|
|
538
632
|
@validate_audit_action
|
|
539
633
|
def bulk_update(self, *args, audit_action=AuditAction.RAISE, **kw):
|
|
@@ -552,29 +646,72 @@ class AuditingQuerySet(models.QuerySet):
|
|
|
552
646
|
from .field_audit import request
|
|
553
647
|
request = request.get()
|
|
554
648
|
audit_events = []
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
569
671
|
|
|
570
672
|
@validate_audit_action
|
|
571
|
-
def update(self,
|
|
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
|
+
"""
|
|
572
680
|
if audit_action is AuditAction.IGNORE:
|
|
573
|
-
return super().update(
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
681
|
+
return super().update(**kw)
|
|
682
|
+
assert audit_action is AuditAction.AUDIT, audit_action
|
|
683
|
+
|
|
684
|
+
fields_to_update = set(kw.keys())
|
|
685
|
+
audited_fields = set(AuditEvent.field_names(self.model))
|
|
686
|
+
fields_to_audit = fields_to_update & audited_fields
|
|
687
|
+
if not fields_to_audit:
|
|
688
|
+
# no audited fields are changing
|
|
689
|
+
return super().update(**kw)
|
|
690
|
+
|
|
691
|
+
new_values = {field: kw[field] for field in fields_to_audit}
|
|
692
|
+
|
|
693
|
+
old_values = {}
|
|
694
|
+
values_to_fetch = fields_to_update | {"pk"}
|
|
695
|
+
for value in self.values(*values_to_fetch):
|
|
696
|
+
pk = value.pop('pk')
|
|
697
|
+
old_values[pk] = value
|
|
698
|
+
|
|
699
|
+
with transaction.atomic(using=self.db):
|
|
700
|
+
rows = super().update(**kw)
|
|
701
|
+
# create and write the audit events _after_ the update succeeds
|
|
702
|
+
from .field_audit import request
|
|
703
|
+
request = request.get()
|
|
704
|
+
audit_events = []
|
|
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
|
|
709
|
+
)
|
|
710
|
+
if audit_event:
|
|
711
|
+
audit_events.append(audit_event)
|
|
712
|
+
if audit_events:
|
|
713
|
+
AuditEvent.objects.bulk_create(audit_events)
|
|
714
|
+
return rows
|
|
578
715
|
|
|
579
716
|
|
|
580
717
|
AuditingManager = models.Manager.from_queryset(AuditingQuerySet)
|
|
File without changes
|
{django-field-audit-1.2.3 → django-field-audit-1.2.5}/django_field_audit.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{django-field-audit-1.2.3 → django-field-audit-1.2.5}/django_field_audit.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/management/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{django-field-audit-1.2.3 → django-field-audit-1.2.5}/field_audit/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|