Option to disable logging on raw save and via context manager (#446)

* Disable on raw save prototype
* Contextmanager to disable instead of just raw - so we can catch m2m relations too
Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
This commit is contained in:
Robin Harms Oredsson 2022-11-04 09:12:06 +01:00 committed by GitHub
parent aa6d977f8b
commit 8fe776ae45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 156 additions and 5 deletions

View file

@ -4,6 +4,8 @@
- feat: Add `serialized_data` field on `LogEntry` model. ([#412](https://github.com/jazzband/django-auditlog/pull/412))
- feat: Display the field name as it would be displayed in Django Admin or use `mapping_field` if available [#428](https://github.com/jazzband/django-auditlog/pull/428)
- feat: New context manager `disable_auditlog` to turn off logging and a new setting `AUDITLOG_DISABLE_ON_RAW_SAVE`
to disable it during raw-save operations like loaddata. [#446](https://github.com/jazzband/django-auditlog/pull/446)
#### Fixes

View file

@ -15,3 +15,8 @@ settings.AUDITLOG_EXCLUDE_TRACKING_MODELS = getattr(
settings.AUDITLOG_INCLUDE_TRACKING_MODELS = getattr(
settings, "AUDITLOG_INCLUDE_TRACKING_MODELS", ()
)
# Disable on raw save to avoid logging imports and similar
settings.AUDITLOG_DISABLE_ON_RAW_SAVE = getattr(
settings, "AUDITLOG_DISABLE_ON_RAW_SAVE", False
)

View file

@ -64,3 +64,15 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs):
instance.actor = user
instance.remote_addr = auditlog["remote_addr"]
@contextlib.contextmanager
def disable_auditlog():
threadlocal.auditlog_disabled = True
try:
yield
finally:
try:
del threadlocal.auditlog_disabled
except AttributeError:
pass

View file

@ -1,9 +1,31 @@
import json
from functools import wraps
from django.conf import settings
from auditlog.context import threadlocal
from auditlog.diff import model_instance_diff
from auditlog.models import LogEntry
def check_disable(signal_handler):
"""
Decorator that passes along disabled in kwargs if any of the following is true:
- 'auditlog_disabled' from threadlocal is true
- raw = True and AUDITLOG_DISABLE_ON_RAW_SAVE is True
"""
@wraps(signal_handler)
def wrapper(*args, **kwargs):
if not getattr(threadlocal, "auditlog_disabled", False) and not (
kwargs.get("raw") and settings.AUDITLOG_DISABLE_ON_RAW_SAVE
):
signal_handler(*args, **kwargs)
return wrapper
@check_disable
def log_create(sender, instance, created, **kwargs):
"""
Signal receiver that creates a log entry when a model instance is first saved to the database.
@ -20,6 +42,7 @@ def log_create(sender, instance, created, **kwargs):
)
@check_disable
def log_update(sender, instance, **kwargs):
"""
Signal receiver that creates a log entry when a model instance is changed and saved to the database.
@ -45,6 +68,7 @@ def log_update(sender, instance, **kwargs):
)
@check_disable
def log_delete(sender, instance, **kwargs):
"""
Signal receiver that creates a log entry when a model instance is deleted from the database.
@ -64,6 +88,7 @@ def log_delete(sender, instance, **kwargs):
def make_log_m2m_changes(field_name):
"""Return a handler for m2m_changed with field_name enclosed."""
@check_disable
def log_m2m_changes(signal, action, **kwargs):
"""Handle m2m_changed and call LogEntry.objects.log_m2m_changes as needed."""
if action not in ["post_add", "post_clear", "post_remove"]:

View file

@ -266,7 +266,8 @@ class AuditlogModelRegistry:
"""
if not isinstance(settings.AUDITLOG_INCLUDE_ALL_MODELS, bool):
raise TypeError("Setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be a boolean")
if not isinstance(settings.AUDITLOG_DISABLE_ON_RAW_SAVE, bool):
raise TypeError("Setting 'AUDITLOG_DISABLE_ON_RAW_SAVE' must be a boolean")
if not isinstance(settings.AUDITLOG_EXCLUDE_TRACKING_MODELS, (list, tuple)):
raise TypeError(
"Setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS' must be a list or tuple"

View file

@ -0,0 +1,15 @@
[
{
"model": "auditlog_tests.manyrelatedmodel",
"pk": 1,
"fields": {
"recursive": [1],
"related": [1]
}
},
{
"model": "auditlog_tests.manyrelatedothermodel",
"pk": 1,
"fields": {}
}
]

View file

@ -12,12 +12,13 @@ from django.contrib.admin.sites import AdminSite
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.core import management
from django.db.models.signals import pre_save
from django.test import RequestFactory, TestCase, override_settings
from django.utils import dateformat, formats, timezone
from auditlog.admin import LogEntryAdmin
from auditlog.context import set_actor
from auditlog.context import disable_auditlog, set_actor
from auditlog.diff import model_instance_diff
from auditlog.middleware import AuditlogMiddleware
from auditlog.models import LogEntry
@ -1092,6 +1093,12 @@ class RegisterModelSettingsTest(TestCase):
):
self.test_auditlog.register_from_settings()
with override_settings(AUDITLOG_DISABLE_ON_RAW_SAVE="bad value"):
with self.assertRaisesMessage(
TypeError, "Setting 'AUDITLOG_DISABLE_ON_RAW_SAVE' must be a boolean"
):
self.test_auditlog.register_from_settings()
@override_settings(
AUDITLOG_INCLUDE_ALL_MODELS=True,
AUDITLOG_EXCLUDE_TRACKING_MODELS=("auditlog_tests.SimpleExcludeModel",),
@ -1797,3 +1804,54 @@ class TestModelSerialization(TestCase):
"value": 11,
},
)
@override_settings(AUDITLOG_DISABLE_ON_RAW_SAVE=True)
class DisableTest(TestCase):
"""
All the other tests check logging, so this only needs to test disabled logging.
"""
def test_create(self):
# Mimic the way imports create objects
inst = SimpleModel(
text="I am a bit more difficult.", boolean=False, datetime=timezone.now()
)
SimpleModel.save_base(inst, raw=True)
self.assertEqual(0, LogEntry.objects.get_for_object(inst).count())
def test_create_with_context_manager(self):
with disable_auditlog():
inst = SimpleModel.objects.create(text="I am a bit more difficult.")
self.assertEqual(0, LogEntry.objects.get_for_object(inst).count())
def test_update(self):
inst = SimpleModel(
text="I am a bit more difficult.", boolean=False, datetime=timezone.now()
)
SimpleModel.save_base(inst, raw=True)
inst.text = "I feel refreshed"
inst.save_base(raw=True)
self.assertEqual(0, LogEntry.objects.get_for_object(inst).count())
def test_update_with_context_manager(self):
inst = SimpleModel(
text="I am a bit more difficult.", boolean=False, datetime=timezone.now()
)
SimpleModel.save_base(inst, raw=True)
with disable_auditlog():
inst.text = "I feel refreshed"
inst.save()
self.assertEqual(0, LogEntry.objects.get_for_object(inst).count())
def test_m2m(self):
"""
Create m2m from fixture and check that nothing was logged.
This only works with context manager
"""
with disable_auditlog():
management.call_command("loaddata", "m2m_test_fixture.json", verbosity=0)
recursive = ManyRelatedModel.objects.get(pk=1)
self.assertEqual(0, LogEntry.objects.get_for_object(recursive).count())
related = ManyRelatedOtherModel.objects.get(pk=1)
self.assertEqual(0, LogEntry.objects.get_for_object(related).count())

View file

@ -201,6 +201,18 @@ It must be a list or tuple. Each item in this setting can be a:
.. versionadded:: 2.1.0
**AUDITLOG_DISABLE_ON_RAW_SAVE**
Disables logging during raw save. (I.e. for instance using loaddata)
.. note::
M2M operations will still be logged, since they're never considered `raw`. To disable them
you must remove their setting or use the `disable_auditlog` context manager.
.. versionadded:: 2.2.0
Actors
------
@ -228,10 +240,11 @@ It is recommended to keep all middleware that alters the request loaded before A
user as actor. To only have some object changes to be logged with the current request's user as actor manual logging is
required.
Context manager
***************
Context managers
----------------
.. versionadded:: 2.1.0
Set actor
*********
To enable the automatic logging of the actors outside of request context (e.g. in a Celery task), you can use a context
manager::
@ -244,6 +257,26 @@ manager::
# if your code here leads to creation of LogEntry instances, these will have the actor set
...
.. versionadded:: 2.1.0
Disable auditlog
****************
Disable auditlog temporary, for instance if you need to install a large fixture on a live system or cleanup
corrupt data::
from auditlog.context import disable_auditlog
with disable_auditlog():
# Do things silently here
...
.. versionadded:: 2.2.0
Object history
--------------