mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-05-20 13:21:52 +00:00
Merge upstream version 2.2.0
This commit is contained in:
commit
c422dd1f0d
19 changed files with 315 additions and 22 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
fail-fast: false
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10']
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
# Changes
|
||||
|
||||
#### Improvements
|
||||
## 2.2.0 (2022-10-07)
|
||||
|
||||
#### Improvements
|
||||
- feat: Add `ACCESS` action to `LogEntry` model and allow object access to be logged. ([#436](https://github.com/jazzband/django-auditlog/pull/436))
|
||||
- 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)
|
||||
- Python: Confirm Python 3.11 support ([#447](https://github.com/jazzband/django-auditlog/pull/447))
|
||||
- feat: Replace the `django.utils.timezone.utc` by `datetime.timezone.utc`. [#448](https://github.com/jazzband/django-auditlog/pull/448)
|
||||
|
||||
#### Fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from datetime import timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import NOT_PROVIDED, DateTimeField, JSONField, Model
|
||||
from django.utils import timezone
|
||||
from django.utils import timezone as django_timezone
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
|
||||
|
|
@ -63,8 +65,12 @@ def get_field_value(obj, field):
|
|||
# DateTimeFields are timezone-aware, so we need to convert the field
|
||||
# to its naive form before we can accurately compare them for changes.
|
||||
value = field.to_python(getattr(obj, field.name, None))
|
||||
if value is not None and settings.USE_TZ and not timezone.is_naive(value):
|
||||
value = timezone.make_naive(value, timezone=timezone.utc)
|
||||
if (
|
||||
value is not None
|
||||
and settings.USE_TZ
|
||||
and not django_timezone.is_naive(value)
|
||||
):
|
||||
value = django_timezone.make_naive(value, timezone=timezone.utc)
|
||||
elif isinstance(field, JSONField):
|
||||
value = field.to_python(getattr(obj, field.name, None))
|
||||
else:
|
||||
|
|
|
|||
21
auditlog/migrations/0014_add_logentry_action_access.py
Normal file
21
auditlog/migrations/0014_add_logentry_action_access.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.1.1 on 2022-10-13 07:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("auditlog", "0013_logentry_serialized_data"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="logentry",
|
||||
name="action",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[(0, "create"), (1, "update"), (2, "delete"), (3, "access")],
|
||||
db_index=True,
|
||||
verbose_name="action",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -15,6 +15,7 @@ from django.utils.timezone import localtime
|
|||
|
||||
from auditlog.models import LogEntry
|
||||
from auditlog.registry import auditlog
|
||||
from auditlog.signals import accessed
|
||||
|
||||
MAX = 75
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ class LogEntryAdminMixin:
|
|||
|
||||
@admin.display(description="Changes")
|
||||
def msg_short(self, obj):
|
||||
if obj.action == LogEntry.Action.DELETE:
|
||||
if obj.action in [LogEntry.Action.DELETE, LogEntry.Action.ACCESS]:
|
||||
return "" # delete
|
||||
changes = json.loads(obj.changes)
|
||||
s = "" if len(changes) == 1 else "s"
|
||||
|
|
@ -177,3 +178,10 @@ class LogEntryAdminMixin:
|
|||
return pretty_name(getattr(field, "verbose_name", field_name))
|
||||
except FieldDoesNotExist:
|
||||
return pretty_name(field_name)
|
||||
|
||||
|
||||
class LogAccessMixin:
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
obj = self.get_object()
|
||||
accessed.send(obj.__class__, instance=obj)
|
||||
return super().render_to_response(context, **response_kwargs)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import ast
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from datetime import timezone
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dateutil import parser
|
||||
|
|
@ -12,7 +13,7 @@ from django.core import serializers
|
|||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db import DEFAULT_DB_ALIAS, models
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.utils import formats, timezone
|
||||
from django.utils import formats
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
|
@ -308,17 +309,20 @@ class LogEntry(models.Model):
|
|||
action. This may be useful in some cases when comparing actions because the ``__lt``, ``__lte``,
|
||||
``__gt``, ``__gte`` lookup filters can be used in queries.
|
||||
|
||||
The valid actions are :py:attr:`Action.CREATE`, :py:attr:`Action.UPDATE` and :py:attr:`Action.DELETE`.
|
||||
The valid actions are :py:attr:`Action.CREATE`, :py:attr:`Action.UPDATE`,
|
||||
:py:attr:`Action.DELETE` and :py:attr:`Action.ACCESS`.
|
||||
"""
|
||||
|
||||
CREATE = 0
|
||||
UPDATE = 1
|
||||
DELETE = 2
|
||||
ACCESS = 3
|
||||
|
||||
choices = (
|
||||
(CREATE, _("create")),
|
||||
(UPDATE, _("update")),
|
||||
(DELETE, _("delete")),
|
||||
(ACCESS, _("access")),
|
||||
)
|
||||
|
||||
content_type = models.ForeignKey(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -61,9 +85,25 @@ def log_delete(sender, instance, **kwargs):
|
|||
)
|
||||
|
||||
|
||||
def log_access(sender, instance, **kwargs):
|
||||
"""
|
||||
Signal receiver that creates a log entry when a model instance is accessed in a AccessLogDetailView.
|
||||
|
||||
Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
|
||||
"""
|
||||
if instance.pk is not None:
|
||||
|
||||
LogEntry.objects.log_create(
|
||||
instance,
|
||||
action=LogEntry.Action.ACCESS,
|
||||
changes="null",
|
||||
)
|
||||
|
||||
|
||||
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"]:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from django.db.models.signals import (
|
|||
)
|
||||
|
||||
from auditlog.conf import settings
|
||||
from auditlog.signals import accessed
|
||||
|
||||
DispatchUID = Tuple[int, int, int]
|
||||
|
||||
|
|
@ -44,10 +45,11 @@ class AuditlogModelRegistry:
|
|||
create: bool = True,
|
||||
update: bool = True,
|
||||
delete: bool = True,
|
||||
access: bool = True,
|
||||
m2m: bool = True,
|
||||
custom: Optional[Dict[ModelSignal, Callable]] = None,
|
||||
):
|
||||
from auditlog.receivers import log_create, log_delete, log_update
|
||||
from auditlog.receivers import log_access, log_create, log_delete, log_update
|
||||
|
||||
self._registry = {}
|
||||
self._signals = {}
|
||||
|
|
@ -59,6 +61,8 @@ class AuditlogModelRegistry:
|
|||
self._signals[pre_save] = log_update
|
||||
if delete:
|
||||
self._signals[post_delete] = log_delete
|
||||
if access:
|
||||
self._signals[accessed] = log_access
|
||||
self._m2m = m2m
|
||||
|
||||
if custom is not None:
|
||||
|
|
@ -267,7 +271,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"
|
||||
|
|
|
|||
3
auditlog/signals.py
Normal file
3
auditlog/signals.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import django.dispatch
|
||||
|
||||
accessed = django.dispatch.Signal()
|
||||
15
auditlog_tests/fixtures/m2m_test_fixture.json
Normal file
15
auditlog_tests/fixtures/m2m_test_fixture.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[
|
||||
{
|
||||
"model": "auditlog_tests.manyrelatedmodel",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"recursive": [1],
|
||||
"related": [1]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "auditlog_tests.manyrelatedothermodel",
|
||||
"pk": 1,
|
||||
"fields": {}
|
||||
}
|
||||
]
|
||||
0
auditlog_tests/templates/simplemodel_detail.html
Normal file
0
auditlog_tests/templates/simplemodel_detail.html
Normal file
|
|
@ -2,6 +2,7 @@ import datetime
|
|||
import itertools
|
||||
import json
|
||||
import warnings
|
||||
from datetime import timezone
|
||||
from unittest import mock
|
||||
|
||||
import freezegun
|
||||
|
|
@ -12,12 +13,15 @@ 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 django.urls import reverse
|
||||
from django.utils import dateformat, formats
|
||||
from django.utils import timezone as django_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
|
||||
|
|
@ -616,8 +620,8 @@ class AdditionalDataModelTest(TestCase):
|
|||
class DateTimeFieldModelTest(TestCase):
|
||||
"""Tests if DateTimeField changes are recognised correctly"""
|
||||
|
||||
utc_plus_one = timezone.get_fixed_timezone(datetime.timedelta(hours=1))
|
||||
now = timezone.now()
|
||||
utc_plus_one = django_timezone.get_fixed_timezone(datetime.timedelta(hours=1))
|
||||
now = django_timezone.now()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -786,7 +790,7 @@ class DateTimeFieldModelTest(TestCase):
|
|||
" DATETIME_FORMAT"
|
||||
),
|
||||
)
|
||||
timestamp = timezone.now()
|
||||
timestamp = django_timezone.now()
|
||||
dtm.timestamp = timestamp
|
||||
dtm.save()
|
||||
localized_timestamp = timestamp.astimezone(gettz(settings.TIME_ZONE))
|
||||
|
|
@ -910,7 +914,9 @@ class DateTimeFieldModelTest(TestCase):
|
|||
dtm.save()
|
||||
|
||||
# Change with naive field doesnt raise error
|
||||
dtm.naive_dt = timezone.make_naive(timezone.now(), timezone=timezone.utc)
|
||||
dtm.naive_dt = django_timezone.make_naive(
|
||||
django_timezone.now(), timezone=timezone.utc
|
||||
)
|
||||
dtm.save()
|
||||
|
||||
|
||||
|
|
@ -1092,6 +1098,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",),
|
||||
|
|
@ -1579,7 +1591,7 @@ class ModelInstanceDiffTest(TestCase):
|
|||
class TestModelSerialization(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.test_date = datetime.datetime(2022, 1, 1, 12, tzinfo=datetime.timezone.utc)
|
||||
self.test_date = datetime.datetime(2022, 1, 1, 12, tzinfo=timezone.utc)
|
||||
self.test_date_string = datetime.datetime.strftime(
|
||||
self.test_date, "%Y-%m-%dT%XZ"
|
||||
)
|
||||
|
|
@ -1796,3 +1808,87 @@ class TestModelSerialization(TestCase):
|
|||
"value": 11,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TestAccessLog(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="test_user", is_active=True)
|
||||
self.obj = SimpleModel.objects.create(text="For admin logentry test")
|
||||
|
||||
def test_access_log(self):
|
||||
self.client.force_login(self.user)
|
||||
content_type = ContentType.objects.get_for_model(self.obj.__class__)
|
||||
|
||||
# Check for log entries
|
||||
qs = LogEntry.objects.filter(content_type=content_type, object_pk=self.obj.pk)
|
||||
old_count = qs.count()
|
||||
|
||||
self.client.get(reverse("simplemodel-detail", args=[self.obj.pk]))
|
||||
new_count = qs.count()
|
||||
self.assertEqual(new_count, old_count + 1)
|
||||
|
||||
log_entry = qs.latest()
|
||||
self.assertEqual(int(log_entry.object_pk), self.obj.pk)
|
||||
self.assertEqual(log_entry.actor, self.user)
|
||||
self.assertEqual(log_entry.content_type, content_type)
|
||||
self.assertEqual(
|
||||
log_entry.action, LogEntry.Action.ACCESS, msg="Action is 'ACCESS'"
|
||||
)
|
||||
self.assertEqual(log_entry.changes, "null")
|
||||
|
||||
|
||||
@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=django_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=django_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=django_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())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from auditlog_tests.views import SimpleModelDetailview
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path(
|
||||
"simplemodel/<int:pk>/",
|
||||
SimpleModelDetailview.as_view(),
|
||||
name="simplemodel-detail",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
9
auditlog_tests/views.py
Normal file
9
auditlog_tests/views.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from django.views.generic import DetailView
|
||||
|
||||
from auditlog.mixins import LogAccessMixin
|
||||
from auditlog_tests.models import SimpleModel
|
||||
|
||||
|
||||
class SimpleModelDetailview(LogAccessMixin, DetailView):
|
||||
model = SimpleModel
|
||||
template_name = "simplemodel_detail.html"
|
||||
|
|
@ -37,6 +37,25 @@ It is recommended to place the register code (``auditlog.register(MyModel)``) at
|
|||
This ensures that every time your model is imported it will also be registered to log changes. Auditlog makes sure that
|
||||
each model is only registered once, otherwise duplicate log entries would occur.
|
||||
|
||||
|
||||
**Logging access**
|
||||
|
||||
By default, Auditlog will only log changes to your model instances. If you want to log access to your model instances as well, Auditlog provides a mixin class for that purpose. Simply add the :py:class:`auditlog.mixins.LogAccessMixin` to your class based view and Auditlog will log access to your model instances. The mixin expects your view to have a ``get_object`` method that returns the model instance for which access shall be logged - this is usually the case for DetailViews and UpdateViews.
|
||||
|
||||
A DetailView utilizing the LogAccessMixin could look like the following example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.views.generic import DetailView
|
||||
|
||||
from auditlog.mixins import LogAccessMixin
|
||||
|
||||
class MyModelDetailView(LogAccessMixin, DetailView):
|
||||
model = MyModel
|
||||
|
||||
# View code goes here
|
||||
|
||||
|
||||
**Excluding fields**
|
||||
|
||||
Fields that are excluded will not trigger saving a new log entry and will not show up in the recorded changes.
|
||||
|
|
@ -201,6 +220,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 +259,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 +276,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
|
||||
--------------
|
||||
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -40,6 +40,7 @@ setup(
|
|||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Framework :: Django",
|
||||
"Framework :: Django :: 3.2",
|
||||
"Framework :: Django :: 4.0",
|
||||
|
|
|
|||
7
tox.ini
7
tox.ini
|
|
@ -1,7 +1,8 @@
|
|||
[tox]
|
||||
envlist =
|
||||
{py37,py38,py39,py310}-django32
|
||||
{py38,py39,py310}-django{40,41,main}
|
||||
{py38,py39,py310}-django40
|
||||
{py38,py39,py310,py311}-django{41,main}
|
||||
py37-docs
|
||||
py38-lint
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ deps =
|
|||
codecov
|
||||
django-multiselectfield
|
||||
freezegun
|
||||
psycopg2-binary==2.8.6
|
||||
psycopg2-binary
|
||||
passenv=
|
||||
TEST_DB_HOST
|
||||
TEST_DB_USER
|
||||
|
|
@ -30,6 +31,7 @@ passenv=
|
|||
TEST_DB_PORT
|
||||
|
||||
basepython =
|
||||
py311: python3.11
|
||||
py310: python3.10
|
||||
py39: python3.9
|
||||
py38: python3.8
|
||||
|
|
@ -51,3 +53,4 @@ python =
|
|||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311
|
||||
|
|
|
|||
Loading…
Reference in a new issue