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.
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/PKG-INFO +11 -10
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/README.md +10 -9
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/django_field_audit.egg-info/PKG-INFO +11 -10
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/__init__.py +1 -1
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/field_audit.py +13 -10
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/management/commands/bootstrap_field_audit_events.py +1 -9
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/models.py +182 -79
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/LICENSE +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/django_field_audit.egg-info/SOURCES.txt +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/django_field_audit.egg-info/dependency_links.txt +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/django_field_audit.egg-info/top_level.txt +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/apps.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/auditors.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/const.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/management/__init__.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/management/commands/__init__.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/middleware.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/migrations/0001_initial.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/migrations/0002_add_is_bootstrap_column.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/migrations/__init__.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/field_audit/utils.py +0 -0
- {django-field-audit-1.2.4 → django-field-audit-1.2.5}/setup.cfg +0 -0
- {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.
|
|
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,
|
|
155
|
-
write methods can actually perform
|
|
156
|
-
`audit_action=AuditAction.AUDIT`.
|
|
157
|
-
implemented and will raise
|
|
158
|
-
|
|
159
|
-
**TODO** below. All four methods do
|
|
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,
|
|
128
|
-
write methods can actually perform
|
|
129
|
-
`audit_action=AuditAction.AUDIT`.
|
|
130
|
-
implemented and will raise
|
|
131
|
-
|
|
132
|
-
**TODO** below. All four methods do
|
|
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.
|
|
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,
|
|
155
|
-
write methods can actually perform
|
|
156
|
-
`audit_action=AuditAction.AUDIT`.
|
|
157
|
-
implemented and will raise
|
|
158
|
-
|
|
159
|
-
**TODO** below. All four methods do
|
|
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,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
|
|
|
@@ -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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
536
|
-
if audit_action is AuditAction.IGNORE:
|
|
537
|
-
return super().bulk_create(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
audit_event = AuditEvent.
|
|
605
|
-
|
|
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)
|
|
File without changes
|
{django-field-audit-1.2.4 → 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.4 → 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.4 → django-field-audit-1.2.5}/field_audit/management/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{django-field-audit-1.2.4 → 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
|