Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ FIELD_AUDIT_AUDITORS = []
|:----------------------------------|:---------------------------------------------------------------|:------------------------
| `FIELD_AUDIT_AUDITEVENT_MANAGER` | A custom manager to use for the `AuditEvent` Model. | `field_audit.models.DefaultAuditEventManager`
| `FIELD_AUDIT_AUDITORS` | A custom list of auditors for acquiring `change_context` info. | `["field_audit.auditors.RequestAuditor", "field_audit.auditors.SystemUserAuditor"]`
| `FIELD_AUDIT_ENABLED` | Global switch to enable/disable all auditing operations. | `True`
| `FIELD_AUDIT_SERVICE_CLASS` | A custom service class for audit logic implementation. | `field_audit.services.AuditService`

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

### Disabling Auditing

There are scenarios where you may want to temporarily or globally disable auditing:

1. **Unit Tests**: Improve test performance by disabling audit overhead
2. **Data Migrations**: Skip auditing during large-scale data operations
3. **Import Operations**: Avoid creating audit events during bulk data imports
4. **Maintenance Operations**: Specific operations that shouldn't be tracked

#### Global Disable via Django Setting

To disable auditing for your entire application, set in your Django settings:

```python
# settings.py
FIELD_AUDIT_ENABLED = False # Auditing disabled globally
```

When this setting is `False`, no audit events will be created anywhere in your application. The default value is `True` (auditing enabled).

#### Runtime Disable via Context Manager

To temporarily disable auditing for a specific block of code:

```python
from field_audit import disable_audit

# Disable auditing for specific operations
with disable_audit():
obj.field1 = "new value"
obj.save() # No audit event created

MyModel.objects.bulk_create(objects) # No audit events
obj.m2m_field.add(other_obj) # No audit event

# Auditing automatically re-enabled after context exits
obj.save() # Audit event created (if FIELD_AUDIT_ENABLED=True)
```

#### Enable Override

You can also temporarily enable auditing even when the global setting is disabled:

```python
from field_audit import enable_audit

# In settings.py: FIELD_AUDIT_ENABLED = False

# Enable auditing for specific operations
with enable_audit():
obj.save() # Audit event IS created despite global setting
```

#### Use Cases

**Unit Tests**: Disable auditing for specific tests to improve performance:

```python
from field_audit import disable_audit

class MyTestCase(TestCase):
def test_without_audit(self):
with disable_audit():
# Fast test without audit overhead
obj = MyModel.objects.create(field1="test")
self.assertEqual(obj.field1, "test")
```

**Data Migrations**: Skip auditing during bulk data operations:

```python
from field_audit import disable_audit

def migrate_data():
with disable_audit():
# Bulk operations without creating audit events
MyModel.objects.filter(status="old").update(status="new")
```

**Thread Safety**: The disable mechanism is thread-safe and async-safe, using Python's `contextvars` module. Each thread/coroutine has its own independent state.

### Using with SQLite

This app uses Django's `JSONField` which means if you intend to use the app with
Expand Down
2 changes: 1 addition & 1 deletion field_audit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .field_audit import audit_fields # noqa: F401
from .field_audit import audit_fields, disable_audit, enable_audit # noqa: F401
from .services import AuditService, get_audit_service # noqa: F401

__version__ = "1.4.0"
11 changes: 11 additions & 0 deletions field_audit/field_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
from django.db import models, router, transaction
from django.db.models.signals import m2m_changed

from .global_context import disable_audit, enable_audit, is_audit_enabled
from .utils import get_fqcn

__all__ = [
"AlreadyAudited",
"audit_fields",
"disable_audit",
"enable_audit",
"get_audited_class_path",
"get_audited_models",
"request",
Expand Down Expand Up @@ -117,6 +120,10 @@ def _decorate_db_write(func):
"""
@wraps(func)
def wrapper(self, *args, **kw):
# Skip auditing if globally disabled
if not is_audit_enabled():
return func(self, *args, **kw)

# for details on using 'self._state', see:
# - https://docs.djangoproject.com/en/dev/ref/models/instances/#state
# - https://stackoverflow.com/questions/907695/
Expand Down Expand Up @@ -189,6 +196,10 @@ def _m2m_changed_handler(sender, instance, action, pk_set, **kwargs):
:param action: A string indicating the type of update
:param pk_set: For add/remove actions, set of primary key values
"""
# Skip auditing if globally disabled
if not is_audit_enabled():
return

from .services import get_audit_service

service = get_audit_service()
Expand Down
58 changes: 58 additions & 0 deletions field_audit/global_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import contextvars
from contextlib import contextmanager

from django.conf import settings

# Context variable for enabling/disabling auditing at runtime
audit_enabled = contextvars.ContextVar("audit_enabled", default=None)


def is_audit_enabled():
"""
Check if auditing is currently enabled.

Returns True if auditing should proceed, False if disabled.
Checks context variable first, then falls back to Django setting.
"""
# Check context variable first (runtime override)
ctx_value = audit_enabled.get()
if ctx_value is not None:
return ctx_value

# Fall back to Django setting (default: True)
return getattr(settings, "FIELD_AUDIT_ENABLED", True)


@contextmanager
def disable_audit():
"""
Context manager to temporarily disable auditing.

Example:
from field_audit import disable_audit

with disable_audit():
# Auditing is disabled in this block
obj.save()
MyModel.objects.bulk_create(objects)
"""
token = audit_enabled.set(False)
try:
yield
finally:
audit_enabled.reset(token)


@contextmanager
def enable_audit():
"""
Context manager to explicitly enable auditing.

Useful when FIELD_AUDIT_ENABLED=False but you need auditing
for a specific block of code.
"""
token = audit_enabled.set(True)
try:
yield
finally:
audit_enabled.reset(token)
Loading