django-field-audit 1.5.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.
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2022, Dimagi Inc., and individual contributors.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name Dimagi, nor the names of its contributors, may be used
12
+ to endorse or promote products derived from this software without
13
+ specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL DIMAGI INC. BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,444 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-field-audit
3
+ Version: 1.5.0
4
+ Summary: Audit Field Changes on Django Models
5
+ Author-email: Joel Miller <jmiller@dimagi.com>, Simon Kelly <simongdkelly@gmail.com>, Graham Herceg <gherceg@dimagi.com>, Chris Smit <chris.smit@dimagi.com>, Daniel Miller <millerdev@gmail.com>
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: Django
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ License-File: LICENSE
22
+ Requires-Dist: django>=3.2
23
+ Project-URL: Homepage, https://github.com/dimagi/django-field-audit
24
+ Project-URL: Repository, https://github.com/dimagi/django-field-audit
25
+
26
+ # Audit Field Changes on Django Models
27
+
28
+ [![tests][tests_badge]][tests_link]
29
+ [![coverage][coverage_badge]][coverage_link]
30
+ [![pypi package][pypi_badge]][pypi_link]
31
+
32
+ [tests_badge]: https://github.com/dimagi/django-field-audit/actions/workflows/tests.yml/badge.svg
33
+ [tests_link]: https://github.com/dimagi/django-field-audit/actions/workflows/tests.yml
34
+ [coverage_badge]: https://github.com/dimagi/django-field-audit/raw/coverage-badge/coverage.svg
35
+ [coverage_link]: https://github.com/dimagi/django-field-audit/actions/workflows/coverage.yml
36
+ [pypi_badge]: https://badge.fury.io/py/django-field-audit.svg
37
+ [pypi_link]: https://pypi.org/project/django-field-audit/
38
+
39
+ A Django app for auditing field changes on database models.
40
+
41
+ ## Installation
42
+ ```
43
+ pip install django-field-audit
44
+ ```
45
+
46
+ ## Documentation
47
+
48
+ <!--
49
+ The [django-field-audit documentation][docs] shows how to use this library to
50
+ audit field changes on Django Models.
51
+
52
+ [docs]: https://dimagi.github.io/django-field-audit/
53
+ -->
54
+
55
+ ### Django Settings
56
+
57
+ To enable the app, add it to your Django `INSTALLED_APPS` configuration and run
58
+ migrations. Settings example:
59
+
60
+ ```python
61
+ INSTALLED_APPS = [
62
+ # ...
63
+ "field_audit",
64
+ ]
65
+ ```
66
+
67
+ The "auditor chain" (see `FIELD_AUDIT_AUDITORS` in the **Custom settings** table
68
+ below) is configured out of the box with the default auditors. If
69
+ `change_context` auditing is desired for authenticated Django requests, add the
70
+ app middleware to your Django `MIDDLEWARE` configuration. For example:
71
+
72
+ ```python
73
+ MIDDLEWARE = [
74
+ # ...
75
+ "field_audit.middleware.FieldAuditMiddleware",
76
+ ]
77
+ ```
78
+
79
+ The audit chain can be updated to use custom auditors (subclasses of
80
+ `field_audit.auditors.BaseAuditor`). If `change_context` auditing is not
81
+ desired, the audit chain can be cleared to avoid extra processing:
82
+
83
+ ```python
84
+ FIELD_AUDIT_AUDITORS = []
85
+ ```
86
+
87
+ #### Custom settings details
88
+
89
+ | Name | Description | Default value when unset
90
+ |:----------------------------------|:---------------------------------------------------------------|:------------------------
91
+ | `FIELD_AUDIT_AUDITEVENT_MANAGER` | A custom manager to use for the `AuditEvent` Model. | `field_audit.models.DefaultAuditEventManager`
92
+ | `FIELD_AUDIT_AUDITORS` | A custom list of auditors for acquiring `change_context` info. | `["field_audit.auditors.RequestAuditor", "field_audit.auditors.SystemUserAuditor"]`
93
+ | `FIELD_AUDIT_ENABLED` | Global switch to enable/disable all auditing operations. | `True`
94
+ | `FIELD_AUDIT_SERVICE_CLASS` | A custom service class for audit logic implementation. | `field_audit.services.AuditService`
95
+
96
+ ### Custom Audit Service
97
+
98
+ The audit logic has been extracted into a separate `AuditService` class to improve separation of concerns and enable easier customization of audit behavior. Users can provide custom audit implementations by subclassing `AuditService` and configuring the `FIELD_AUDIT_SERVICE_CLASS` setting.
99
+
100
+ #### Creating a Custom Audit Service
101
+
102
+ ```python
103
+ # myapp/audit.py
104
+
105
+ from field_audit import AuditService
106
+
107
+ class CustomAuditService(AuditService):
108
+ def get_field_value(self, instance, field_name, bootstrap=False):
109
+ # Custom logic for extracting field values
110
+ value = super().get_field_value(instance, field_name, bootstrap)
111
+
112
+ # Example: custom serialization or transformation
113
+ if field_name == 'sensitive_field':
114
+ value = '[REDACTED]'
115
+
116
+ return value
117
+ ```
118
+
119
+ Then configure it in your Django settings:
120
+
121
+ ```python
122
+ # settings.py
123
+
124
+ FIELD_AUDIT_SERVICE_CLASS = 'myapp.audit.CustomAuditService'
125
+ ```
126
+
127
+ #### Backward Compatibility
128
+
129
+ The original `AuditEvent` class methods are maintained for backward compatibility but are now deprecated in favor of the service-based approach. These methods will issue deprecation warnings and delegate to the configured audit service.
130
+
131
+ ### Model Auditing
132
+
133
+ To begin auditing Django models, import the `field_audit.audit_fields` decorator
134
+ and decorate models specifying which fields should be audited for changes.
135
+ Example code:
136
+
137
+ ```python
138
+ # flight/models.py
139
+
140
+ from django.db import models
141
+ from field_audit import audit_fields
142
+
143
+
144
+ @audit_fields("tail_number", "make_model", "operated_by")
145
+ class Aircraft(models.Model):
146
+ id = AutoField(primary_key=True)
147
+ tail_number = models.CharField(max_length=32, unique=True)
148
+ make_model = models.CharField(max_length=64)
149
+ operated_by = models.CharField(max_length=64)
150
+ ```
151
+
152
+ #### Audited DB write operations
153
+
154
+ By default, Model and QuerySet methods are audited, with the exception of four
155
+ "special" QuerySet methods:
156
+
157
+ | DB Write Method | Audited
158
+ |:------------------------------|:-------
159
+ | `Model.delete()` | Yes
160
+ | `Model.save()` | Yes
161
+ | `QuerySet.bulk_create()` | No
162
+ | `QuerySet.bulk_update()` | No
163
+ | `QuerySet.create()` | Yes (via `Model.save()`)
164
+ | `QuerySet.delete()` | No
165
+ | `QuerySet.get_or_create()` | Yes (via `QuerySet.create()`)
166
+ | `QuerySet.update()` | No
167
+ | `QuerySet.update_or_create()` | Yes (via `QuerySet.get_or_create()` and `Model.save()`)
168
+
169
+ #### Auditing Special QuerySet Writes
170
+
171
+ Auditing for the four "special" QuerySet methods that perform DB writes (labeled
172
+ **No** in the table above) _can_ be enabled. This requires three extra usage
173
+ details:
174
+
175
+ > **Warning**
176
+ > Enabling auditing on these QuerySet methods might have significant
177
+ > performance implications, especially on large datasets, since audit events are
178
+ > constructed in memory and bulk written to the database.
179
+
180
+ 1. Enable the feature by calling the audit decorator specifying
181
+ `@audit_fields(..., audit_special_queryset_writes=True)`.
182
+ 2. Configure the model class so its default manager is an instance of
183
+ `field_audit.models.AuditingManager`.
184
+ 3. All calls to the four "special" QuerySet write methods require an extra
185
+ `audit_action` keyword argument whose value is one of:
186
+ - `field_audit.models.AuditAction.AUDIT`
187
+ - `field_audit.models.AuditAction.IGNORE`
188
+
189
+ ##### Important Notes
190
+
191
+ - Specifying `audit_special_queryset_writes=True` (step **1** above) without
192
+ setting the default manager to an instance of `AuditingManager` (step **2**
193
+ above) will raise an exception when the model class is evaluated.
194
+ - At this time, `QuerySet.delete()`, `QuerySet.update()`,
195
+ and `QuerySet.bulk_create()` "special" write methods can actually perform
196
+ change auditing when called with `audit_action=AuditAction.AUDIT`.
197
+ `QuerySet.bulk_update()` is not currently implemented and will raise
198
+ `NotImplementedError` if called with that action. Implementing this remaining
199
+ method remains a task for the future, see **TODO** below. All four methods do
200
+ support `audit_action=AuditAction.IGNORE` usage, however.
201
+ - All audited methods use transactions to ensure changes to audited models
202
+ are only committed to the database if audit events are successfully created
203
+ and saved as well.
204
+
205
+ ### Auditing Many-to-Many fields
206
+
207
+ Many-to-Many field changes are automatically audited through Django signals when
208
+ included in the `@audit_fields` decorator. Changes to M2M relationships generate
209
+ audit events immediately without requiring `save()` calls.
210
+
211
+ ```python
212
+ # Example model with audited M2M field
213
+ @audit_fields("name", "title", "certifications")
214
+ class CrewMember(models.Model):
215
+ name = models.CharField(max_length=256)
216
+ title = models.CharField(max_length=64)
217
+ certifications = models.ManyToManyField('Certification', blank=True)
218
+ ```
219
+
220
+ #### Supported M2M operations
221
+
222
+ All standard M2M operations create audit events:
223
+
224
+ ```python
225
+ crew_member = CrewMember.objects.create(name='Test Pilot', title='Captain')
226
+ cert1 = Certification.objects.create(name='PPL', certification_type='Private')
227
+
228
+ crew_member.certifications.add(cert1) # Creates audit event
229
+ crew_member.certifications.remove(cert1) # Creates audit event
230
+ crew_member.certifications.set([cert1]) # Creates audit event
231
+ crew_member.certifications.clear() # Creates audit event
232
+ ```
233
+
234
+ #### M2M audit event structure
235
+
236
+ M2M changes use specific delta structures in audit events:
237
+
238
+ - **Add**: `{'certifications': {'add': [1, 2]}}`
239
+ - **Remove**: `{'certifications': {'remove': [2]}}`
240
+ - **Clear**: `{'certifications': {'remove': [1, 2]}}`
241
+ - **Create** / **Bootstrap**: `{'certifications': {'new': []}}`
242
+
243
+ #### Bootstrap events for models with existing records
244
+
245
+ In the scenario where auditing is enabled for a model with existing data, it can
246
+ be valuable to generate "bootstrap" audit events for all of the existing model
247
+ records in order to ensure that there is at least one audit event record for
248
+ every model instance that currently exists. There is a migration utility for
249
+ performing this bootstrap operation. Example code:
250
+
251
+ ```python
252
+ # flight/migrations/0002_bootstrap_aircarft_auditing.py
253
+
254
+ from django.db import migrations, models
255
+ from field_audit.utils import run_bootstrap
256
+
257
+ from flight.models import Aircraft
258
+
259
+
260
+ class Migration(migrations.Migration):
261
+
262
+ dependencies = [
263
+ ('flight', '0001_initial'),
264
+ ]
265
+
266
+ operations = [
267
+ run_bootstrap(Aircraft, ["tail_number", "make_model", "operated_by"])
268
+ ]
269
+ ```
270
+
271
+ ##### Bootstrap events via management command
272
+
273
+ If bootstrapping is not suitable during migrations, there is a management command for
274
+ performing the same operation. The management command does not accept arbitrary
275
+ field names for bootstrap records, and uses the fields configured by the
276
+ existing `audit_fields(...)` decorator on the model. Example (analogous to
277
+ migration action shown above):
278
+
279
+ ```sh
280
+ manage.py bootstrap_field_audit_events init Aircraft
281
+ ```
282
+
283
+ Additionally, if a post-migration bootstrap "top up" action is needed, the
284
+ the management command can also perform this action. A "top up" operation
285
+ creates bootstrap audit events for any existing model records which do not have
286
+ a "create" or "bootstrap" `AuditEvent` record. Note that the management command
287
+ is currently the only way to "top up" bootstrap audit events. Example:
288
+
289
+ ```sh
290
+ manage.py bootstrap_field_audit_events top-up Aircraft
291
+ ```
292
+
293
+ ### Disabling Auditing
294
+
295
+ There are scenarios where you may want to temporarily or globally disable auditing:
296
+
297
+ 1. **Unit Tests**: Improve test performance by disabling audit overhead
298
+ 2. **Data Migrations**: Skip auditing during large-scale data operations
299
+ 3. **Import Operations**: Avoid creating audit events during bulk data imports
300
+ 4. **Maintenance Operations**: Specific operations that shouldn't be tracked
301
+
302
+ #### Global Disable via Django Setting
303
+
304
+ To disable auditing for your entire application, set in your Django settings:
305
+
306
+ ```python
307
+ # settings.py
308
+ FIELD_AUDIT_ENABLED = False # Auditing disabled globally
309
+ ```
310
+
311
+ When this setting is `False`, no audit events will be created anywhere in your application. The default value is `True` (auditing enabled).
312
+
313
+ #### Runtime Disable via Context Manager
314
+
315
+ To temporarily disable auditing for a specific block of code:
316
+
317
+ ```python
318
+ from field_audit import disable_audit
319
+
320
+ # Disable auditing for specific operations
321
+ with disable_audit():
322
+ obj.field1 = "new value"
323
+ obj.save() # No audit event created
324
+
325
+ MyModel.objects.bulk_create(objects) # No audit events
326
+ obj.m2m_field.add(other_obj) # No audit event
327
+
328
+ # Auditing automatically re-enabled after context exits
329
+ obj.save() # Audit event created (if FIELD_AUDIT_ENABLED=True)
330
+ ```
331
+
332
+ #### Enable Override
333
+
334
+ You can also temporarily enable auditing even when the global setting is disabled:
335
+
336
+ ```python
337
+ from field_audit import enable_audit
338
+
339
+ # In settings.py: FIELD_AUDIT_ENABLED = False
340
+
341
+ # Enable auditing for specific operations
342
+ with enable_audit():
343
+ obj.save() # Audit event IS created despite global setting
344
+ ```
345
+
346
+ #### Use Cases
347
+
348
+ **Unit Tests**: Disable auditing for specific tests to improve performance:
349
+
350
+ ```python
351
+ from field_audit import disable_audit
352
+
353
+ class MyTestCase(TestCase):
354
+ def test_without_audit(self):
355
+ with disable_audit():
356
+ # Fast test without audit overhead
357
+ obj = MyModel.objects.create(field1="test")
358
+ self.assertEqual(obj.field1, "test")
359
+ ```
360
+
361
+ **Data Migrations**: Skip auditing during bulk data operations:
362
+
363
+ ```python
364
+ from field_audit import disable_audit
365
+
366
+ def migrate_data():
367
+ with disable_audit():
368
+ # Bulk operations without creating audit events
369
+ MyModel.objects.filter(status="old").update(status="new")
370
+ ```
371
+
372
+ **Thread Safety**: The disable mechanism is thread-safe and async-safe, using Python's `contextvars` module. Each thread/coroutine has its own independent state.
373
+
374
+ ### Using with SQLite
375
+
376
+ This app uses Django's `JSONField` which means if you intend to use the app with
377
+ a SQLite database, the SQLite `JSON1` extension is required. If your system's
378
+ Python `sqlite3` library doesn't ship with this extension enabled, see
379
+ [this article](https://code.djangoproject.com/wiki/JSON1Extension) for details
380
+ on how to enable it.
381
+
382
+
383
+ ## Contributing
384
+
385
+ All feature and bug contributions are expected to be covered by tests.
386
+
387
+ ### Setup for developers
388
+
389
+ This project uses [uv](https://docs.astral.sh/uv/) for dependency management. Install uv and then install the project dependencies:
390
+
391
+ ```shell
392
+ cd django-field-audit
393
+ uv sync
394
+ ```
395
+
396
+ ### Running tests
397
+
398
+ **Note**: By default, local tests use an in-memory SQLite database. Ensure that
399
+ your local Python's `sqlite3` library ships with the `JSON1` extension enabled
400
+ (see [Using with SQLite](#using-with-sqlite)).
401
+
402
+ - Tests
403
+ ```shell
404
+ uv run pytest
405
+ ```
406
+
407
+ - Style check
408
+ ```shell
409
+ ruff check
410
+ ```
411
+
412
+ - Coverage
413
+ ```shell
414
+ uv run coverage run -m pytest
415
+ uv run coverage report -m
416
+ ```
417
+
418
+ ### Adding migrations
419
+
420
+ The example `manage.py` is available for making new migrations.
421
+
422
+ ```shell
423
+ uv run python example/manage.py makemigrations field_audit
424
+ ```
425
+
426
+ ### Publishing a new version to PyPI
427
+
428
+ Push a new tag to Github using the format vX.Y.Z where X.Y.Z matches the version
429
+ in [`__init__.py`](field_audit/__init__.py). Also ensure that the changelog is up to date.
430
+
431
+ Publishing is automated with [Github Actions](.github/workflows/pypi.yml).
432
+
433
+ ## TODO
434
+
435
+ - Implement auditing for the remaining "special" QuerySet write operations:
436
+ - `bulk_update()`
437
+ - Write full library documentation using github.io.
438
+
439
+ ### Backlog
440
+
441
+ - Add to optimization for `instance.save(save_fields=[...])` [maybe].
442
+ - Support adding new audit fields on the same model at different times (instead
443
+ of raising `AlreadyAudited`) [maybe].
444
+