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.
- django_field_audit-1.5.0/LICENSE +24 -0
- django_field_audit-1.5.0/PKG-INFO +444 -0
- django_field_audit-1.5.0/README.md +418 -0
- django_field_audit-1.5.0/field_audit/__init__.py +4 -0
- django_field_audit-1.5.0/field_audit/apps.py +11 -0
- django_field_audit-1.5.0/field_audit/auditors.py +125 -0
- django_field_audit-1.5.0/field_audit/const.py +19 -0
- django_field_audit-1.5.0/field_audit/field_audit.py +266 -0
- django_field_audit-1.5.0/field_audit/global_context.py +58 -0
- django_field_audit-1.5.0/field_audit/management/__init__.py +0 -0
- django_field_audit-1.5.0/field_audit/management/commands/__init__.py +0 -0
- django_field_audit-1.5.0/field_audit/management/commands/bootstrap_field_audit_events.py +127 -0
- django_field_audit-1.5.0/field_audit/middleware.py +17 -0
- django_field_audit-1.5.0/field_audit/migrations/0001_initial.py +37 -0
- django_field_audit-1.5.0/field_audit/migrations/0002_add_is_bootstrap_column.py +31 -0
- django_field_audit-1.5.0/field_audit/migrations/0003_alter_auditevent_change_context_and_more.py +32 -0
- django_field_audit-1.5.0/field_audit/migrations/__init__.py +0 -0
- django_field_audit-1.5.0/field_audit/models.py +804 -0
- django_field_audit-1.5.0/field_audit/services.py +385 -0
- django_field_audit-1.5.0/field_audit/utils.py +91 -0
- django_field_audit-1.5.0/pyproject.toml +71 -0
|
@@ -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
|
+
|