Skip to content

Commit a1b6b50

Browse files
authored
Merge pull request #43 from dimagi/sk/skip-audit
Add Global Auditing Disable Feature
2 parents 760d012 + 4eb7d1f commit a1b6b50

File tree

7 files changed

+334
-34
lines changed

7 files changed

+334
-34
lines changed

README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ FIELD_AUDIT_AUDITORS = []
6565
|:----------------------------------|:---------------------------------------------------------------|:------------------------
6666
| `FIELD_AUDIT_AUDITEVENT_MANAGER` | A custom manager to use for the `AuditEvent` Model. | `field_audit.models.DefaultAuditEventManager`
6767
| `FIELD_AUDIT_AUDITORS` | A custom list of auditors for acquiring `change_context` info. | `["field_audit.auditors.RequestAuditor", "field_audit.auditors.SystemUserAuditor"]`
68+
| `FIELD_AUDIT_ENABLED` | Global switch to enable/disable all auditing operations. | `True`
6869
| `FIELD_AUDIT_SERVICE_CLASS` | A custom service class for audit logic implementation. | `field_audit.services.AuditService`
6970

7071
### Custom Audit Service
@@ -264,6 +265,87 @@ is currently the only way to "top up" bootstrap audit events. Example:
264265
manage.py bootstrap_field_audit_events top-up Aircraft
265266
```
266267

268+
### Disabling Auditing
269+
270+
There are scenarios where you may want to temporarily or globally disable auditing:
271+
272+
1. **Unit Tests**: Improve test performance by disabling audit overhead
273+
2. **Data Migrations**: Skip auditing during large-scale data operations
274+
3. **Import Operations**: Avoid creating audit events during bulk data imports
275+
4. **Maintenance Operations**: Specific operations that shouldn't be tracked
276+
277+
#### Global Disable via Django Setting
278+
279+
To disable auditing for your entire application, set in your Django settings:
280+
281+
```python
282+
# settings.py
283+
FIELD_AUDIT_ENABLED = False # Auditing disabled globally
284+
```
285+
286+
When this setting is `False`, no audit events will be created anywhere in your application. The default value is `True` (auditing enabled).
287+
288+
#### Runtime Disable via Context Manager
289+
290+
To temporarily disable auditing for a specific block of code:
291+
292+
```python
293+
from field_audit import disable_audit
294+
295+
# Disable auditing for specific operations
296+
with disable_audit():
297+
obj.field1 = "new value"
298+
obj.save() # No audit event created
299+
300+
MyModel.objects.bulk_create(objects) # No audit events
301+
obj.m2m_field.add(other_obj) # No audit event
302+
303+
# Auditing automatically re-enabled after context exits
304+
obj.save() # Audit event created (if FIELD_AUDIT_ENABLED=True)
305+
```
306+
307+
#### Enable Override
308+
309+
You can also temporarily enable auditing even when the global setting is disabled:
310+
311+
```python
312+
from field_audit import enable_audit
313+
314+
# In settings.py: FIELD_AUDIT_ENABLED = False
315+
316+
# Enable auditing for specific operations
317+
with enable_audit():
318+
obj.save() # Audit event IS created despite global setting
319+
```
320+
321+
#### Use Cases
322+
323+
**Unit Tests**: Disable auditing for specific tests to improve performance:
324+
325+
```python
326+
from field_audit import disable_audit
327+
328+
class MyTestCase(TestCase):
329+
def test_without_audit(self):
330+
with disable_audit():
331+
# Fast test without audit overhead
332+
obj = MyModel.objects.create(field1="test")
333+
self.assertEqual(obj.field1, "test")
334+
```
335+
336+
**Data Migrations**: Skip auditing during bulk data operations:
337+
338+
```python
339+
from field_audit import disable_audit
340+
341+
def migrate_data():
342+
with disable_audit():
343+
# Bulk operations without creating audit events
344+
MyModel.objects.filter(status="old").update(status="new")
345+
```
346+
347+
**Thread Safety**: The disable mechanism is thread-safe and async-safe, using Python's `contextvars` module. Each thread/coroutine has its own independent state.
348+
267349
### Using with SQLite
268350

269351
This app uses Django's `JSONField` which means if you intend to use the app with

field_audit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .field_audit import audit_fields # noqa: F401
1+
from .field_audit import audit_fields, disable_audit, enable_audit # noqa: F401
22
from .services import AuditService, get_audit_service # noqa: F401
33

44
__version__ = "1.4.0"

field_audit/field_audit.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
from django.db import models, router, transaction
55
from django.db.models.signals import m2m_changed
66

7+
from .global_context import disable_audit, enable_audit, is_audit_enabled
78
from .utils import get_fqcn
89

910
__all__ = [
1011
"AlreadyAudited",
1112
"audit_fields",
13+
"disable_audit",
14+
"enable_audit",
1215
"get_audited_class_path",
1316
"get_audited_models",
1417
"request",
@@ -117,6 +120,10 @@ def _decorate_db_write(func):
117120
"""
118121
@wraps(func)
119122
def wrapper(self, *args, **kw):
123+
# Skip auditing if globally disabled
124+
if not is_audit_enabled():
125+
return func(self, *args, **kw)
126+
120127
# for details on using 'self._state', see:
121128
# - https://docs.djangoproject.com/en/dev/ref/models/instances/#state
122129
# - https://stackoverflow.com/questions/907695/
@@ -189,6 +196,10 @@ def _m2m_changed_handler(sender, instance, action, pk_set, **kwargs):
189196
:param action: A string indicating the type of update
190197
:param pk_set: For add/remove actions, set of primary key values
191198
"""
199+
# Skip auditing if globally disabled
200+
if not is_audit_enabled():
201+
return
202+
192203
from .services import get_audit_service
193204

194205
service = get_audit_service()

field_audit/global_context.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import contextvars
2+
from contextlib import contextmanager
3+
4+
from django.conf import settings
5+
6+
# Context variable for enabling/disabling auditing at runtime
7+
audit_enabled = contextvars.ContextVar("audit_enabled", default=None)
8+
9+
10+
def is_audit_enabled():
11+
"""
12+
Check if auditing is currently enabled.
13+
14+
Returns True if auditing should proceed, False if disabled.
15+
Checks context variable first, then falls back to Django setting.
16+
"""
17+
# Check context variable first (runtime override)
18+
ctx_value = audit_enabled.get()
19+
if ctx_value is not None:
20+
return ctx_value
21+
22+
# Fall back to Django setting (default: True)
23+
return getattr(settings, "FIELD_AUDIT_ENABLED", True)
24+
25+
26+
@contextmanager
27+
def disable_audit():
28+
"""
29+
Context manager to temporarily disable auditing.
30+
31+
Example:
32+
from field_audit import disable_audit
33+
34+
with disable_audit():
35+
# Auditing is disabled in this block
36+
obj.save()
37+
MyModel.objects.bulk_create(objects)
38+
"""
39+
token = audit_enabled.set(False)
40+
try:
41+
yield
42+
finally:
43+
audit_enabled.reset(token)
44+
45+
46+
@contextmanager
47+
def enable_audit():
48+
"""
49+
Context manager to explicitly enable auditing.
50+
51+
Useful when FIELD_AUDIT_ENABLED=False but you need auditing
52+
for a specific block of code.
53+
"""
54+
token = audit_enabled.set(True)
55+
try:
56+
yield
57+
finally:
58+
audit_enabled.reset(token)

0 commit comments

Comments
 (0)