mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-03-16 22:20:26 +00:00
Added ACCESS action and enabled logging of object accesses (#436)
This commit is contained in:
parent
8fe776ae45
commit
f71699a9d0
12 changed files with 122 additions and 4 deletions
|
|
@ -1,7 +1,7 @@
|
|||
# Changes
|
||||
|
||||
#### 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`
|
||||
|
|
|
|||
22
auditlog/migrations/0012_add_logentry_action_access.py
Normal file
22
auditlog/migrations/0012_add_logentry_action_access.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.1.1 on 2022-10-13 07:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auditlog", "0011_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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -12,6 +12,7 @@ from django.utils.timezone import localtime
|
|||
|
||||
from auditlog.models import LogEntry
|
||||
from auditlog.registry import auditlog
|
||||
from auditlog.signals import accessed
|
||||
|
||||
MAX = 75
|
||||
|
||||
|
|
@ -50,7 +51,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"
|
||||
|
|
@ -137,3 +138,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)
|
||||
|
|
|
|||
|
|
@ -308,17 +308,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(
|
||||
|
|
|
|||
|
|
@ -85,6 +85,21 @@ 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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
3
auditlog/signals.py
Normal file
3
auditlog/signals.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import django.dispatch
|
||||
|
||||
accessed = django.dispatch.Signal()
|
||||
0
auditlog_tests/templates/simplemodel_detail.html
Normal file
0
auditlog_tests/templates/simplemodel_detail.html
Normal file
|
|
@ -15,6 +15,7 @@ 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.urls import reverse
|
||||
from django.utils import dateformat, formats, timezone
|
||||
|
||||
from auditlog.admin import LogEntryAdmin
|
||||
|
|
@ -1806,6 +1807,33 @@ class TestModelSerialization(TestCase):
|
|||
)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue