Added ACCESS action and enabled logging of object accesses (#436)

This commit is contained in:
Simon Kern 2022-11-07 08:51:00 +01:00 committed by GitHub
parent 8fe776ae45
commit f71699a9d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 122 additions and 4 deletions

View file

@ -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`

View 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",
),
),
]

View file

@ -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)

View file

@ -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(

View file

@ -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."""

View file

@ -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
View file

@ -0,0 +1,3 @@
import django.dispatch
accessed = django.dispatch.Signal()

View 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):
"""

View file

@ -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
View 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"

View file

@ -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.