WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Draft
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
29 changes: 29 additions & 0 deletions auditlog/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
from __future__ import annotations

from importlib.metadata import version
from typing import TYPE_CHECKING

from django.apps import apps as django_apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

if TYPE_CHECKING:
from auditlog.models import AbstractLogEntry

__version__ = version("django-auditlog")


def get_logentry_model() -> type[AbstractLogEntry]:
"""
Return the LogEntry model that is active in this project.
"""
try:
return django_apps.get_model(
settings.AUDITLOG_LOGENTRY_MODEL, require_ready=False
)
except ValueError:
raise ImproperlyConfigured(
"AUDITLOG_LOGENTRY_MODEL must be of the form 'app_label.model_name'"
)
except LookupError:
raise ImproperlyConfigured(
"AUDITLOG_LOGENTRY_MODEL refers to model '%s' that has not been installed"
% settings.AUDITLOG_LOGENTRY_MODEL
)
4 changes: 3 additions & 1 deletion auditlog/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _

from auditlog import get_logentry_model
from auditlog.filters import CIDFilter, ResourceTypeFilter
from auditlog.mixins import LogEntryAdminMixin
from auditlog.models import LogEntry

LogEntry = get_logentry_model()


@admin.register(LogEntry)
Expand Down
5 changes: 5 additions & 0 deletions auditlog/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@
settings.AUDITLOG_CHANGE_DISPLAY_TRUNCATE_LENGTH = getattr(
settings, "AUDITLOG_CHANGE_DISPLAY_TRUNCATE_LENGTH", 140
)

# Swap default model
settings.AUDITLOG_LOGENTRY_MODEL = getattr(
settings, "AUDITLOG_LOGENTRY_MODEL", "auditlog.LogEntry"
)
5 changes: 4 additions & 1 deletion auditlog/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
from django.contrib.auth import get_user_model
from django.db.models.signals import pre_save

from auditlog.models import LogEntry
from auditlog import get_logentry_model

auditlog_value = ContextVar("auditlog_value")
auditlog_disabled = ContextVar("auditlog_disabled", default=False)


LogEntry = get_logentry_model()


@contextlib.contextmanager
def set_actor(actor, remote_addr=None, remote_port=None):
"""Connect a signal receiver with current user attached."""
Expand Down
4 changes: 3 additions & 1 deletion auditlog/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ def track_field(field):
:return: Whether the given field should be tracked.
:rtype: bool
"""
from auditlog.models import LogEntry
from auditlog import get_logentry_model

LogEntry = get_logentry_model()

# Do not track many to many relations
if field.many_to_many:
Expand Down
4 changes: 3 additions & 1 deletion auditlog/management/commands/auditlogflush.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from django.core.management.base import BaseCommand
from django.db import connection

from auditlog.models import LogEntry
from auditlog import get_logentry_model

LogEntry = get_logentry_model()


class Command(BaseCommand):
Expand Down
4 changes: 3 additions & 1 deletion auditlog/management/commands/auditlogmigratejson.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from django.core.management import CommandError, CommandParser
from django.core.management.base import BaseCommand

from auditlog.models import LogEntry
from auditlog import get_logentry_model

LogEntry = get_logentry_model()


class Command(BaseCommand):
Expand Down
1 change: 1 addition & 0 deletions auditlog/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Migration(migrations.Migration):
),
],
options={
"swappable": "AUDITLOG_LOGENTRY_MODEL",
"ordering": ["-timestamp"],
"get_latest_by": "timestamp",
"verbose_name": "log entry",
Expand Down
4 changes: 3 additions & 1 deletion auditlog/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
from django.utils.timezone import is_aware, localtime
from django.utils.translation import gettext_lazy as _

from auditlog.models import LogEntry
from auditlog import get_logentry_model
from auditlog.registry import auditlog
from auditlog.signals import accessed

MAX = 75

LogEntry = get_logentry_model()


class LogEntryAdminMixin:
request: HttpRequest
Expand Down
33 changes: 20 additions & 13 deletions auditlog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from django.utils.encoding import smart_str
from django.utils.translation import gettext_lazy as _

from auditlog import get_logentry_model
from auditlog.diff import mask_str

DEFAULT_OBJECT_REPR = "<error forming object repr>"
Expand Down Expand Up @@ -107,7 +108,7 @@ def log_m2m_changes(
except ObjectDoesNotExist:
object_repr = DEFAULT_OBJECT_REPR
kwargs.setdefault("object_repr", object_repr)
kwargs.setdefault("action", LogEntry.Action.UPDATE)
kwargs.setdefault("action", get_logentry_model().Action.UPDATE)

if isinstance(pk, int):
kwargs.setdefault("object_id", pk)
Expand Down Expand Up @@ -302,17 +303,7 @@ def _mask_serialized_fields(
return data


class LogEntry(models.Model):
"""
Represents an entry in the audit log. The content type is saved along with the textual and numeric
(if available) primary key, as well as the textual representation of the object when it was saved.
It holds the action performed and the fields that were changed in the transaction.

If AuditlogMiddleware is used, the actor will be set automatically. Keep in mind that
editing / re-saving LogEntry instances may set the actor to a wrong value - editing LogEntry
instances is not recommended (and it should not be necessary).
"""

class AbstractLogEntry(models.Model):
class Action:
"""
The actions that Auditlog distinguishes: creating, updating and deleting objects. Viewing objects
Expand Down Expand Up @@ -391,6 +382,7 @@ class Action:
objects = LogEntryManager()

class Meta:
abstract = True
get_latest_by = "timestamp"
ordering = ["-timestamp"]
verbose_name = _("log entry")
Expand Down Expand Up @@ -550,6 +542,21 @@ def _get_changes_display_for_fk_field(
return f"Deleted '{field.related_model.__name__}' ({value})"


class LogEntry(AbstractLogEntry):
"""
Represents an entry in the audit log. The content type is saved along with the textual and numeric
(if available) primary key, as well as the textual representation of the object when it was saved.
It holds the action performed and the fields that were changed in the transaction.

If AuditlogMiddleware is used, the actor will be set automatically. Keep in mind that
editing / re-saving LogEntry instances may set the actor to a wrong value - editing LogEntry
instances is not recommended (and it should not be necessary).
"""

class Meta(AbstractLogEntry.Meta):
swappable = "AUDITLOG_LOGENTRY_MODEL"


class AuditlogHistoryField(GenericRelation):
"""
A subclass of py:class:`django.contrib.contenttypes.fields.GenericRelation` that sets some default
Expand All @@ -570,7 +577,7 @@ class AuditlogHistoryField(GenericRelation):
"""

def __init__(self, pk_indexable=True, delete_related=False, **kwargs):
kwargs["to"] = LogEntry
kwargs["to"] = get_logentry_model()

if pk_indexable:
kwargs["object_id_field"] = "object_id"
Expand Down
16 changes: 8 additions & 8 deletions auditlog/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from django.conf import settings

from auditlog import get_logentry_model
from auditlog.context import auditlog_disabled
from auditlog.diff import model_instance_diff
from auditlog.models import LogEntry
from auditlog.signals import post_log, pre_log


Expand Down Expand Up @@ -38,7 +38,7 @@ def log_create(sender, instance, created, **kwargs):
"""
if created:
_create_log_entry(
action=LogEntry.Action.CREATE,
action=get_logentry_model().Action.CREATE,
instance=instance,
sender=sender,
diff_old=None,
Expand All @@ -57,7 +57,7 @@ def log_update(sender, instance, **kwargs):
update_fields = kwargs.get("update_fields", None)
old = sender._default_manager.filter(pk=instance.pk).first()
_create_log_entry(
action=LogEntry.Action.UPDATE,
action=get_logentry_model().Action.UPDATE,
instance=instance,
sender=sender,
diff_old=old,
Expand All @@ -75,7 +75,7 @@ def log_delete(sender, instance, **kwargs):
"""
if instance.pk is not None:
_create_log_entry(
action=LogEntry.Action.DELETE,
action=get_logentry_model().Action.DELETE,
instance=instance,
sender=sender,
diff_old=instance,
Expand All @@ -91,7 +91,7 @@ def log_access(sender, instance, **kwargs):
"""
if instance.pk is not None:
_create_log_entry(
action=LogEntry.Action.ACCESS,
action=get_logentry_model().Action.ACCESS,
instance=instance,
sender=sender,
diff_old=None,
Expand Down Expand Up @@ -121,7 +121,7 @@ def _create_log_entry(
)

if force_log or changes:
log_entry = LogEntry.objects.log_create(
log_entry = get_logentry_model().objects.log_create(
instance,
action=action,
changes=changes,
Expand Down Expand Up @@ -163,14 +163,14 @@ def log_m2m_changes(signal, action, **kwargs):
)

if action in ["post_add"]:
LogEntry.objects.log_m2m_changes(
get_logentry_model().objects.log_m2m_changes(
changed_queryset,
kwargs["instance"],
"add",
field_name,
)
elif action in ["post_remove", "post_clear"]:
LogEntry.objects.log_m2m_changes(
get_logentry_model().objects.log_m2m_changes(
changed_queryset,
kwargs["instance"],
"delete",
Expand Down
2 changes: 1 addition & 1 deletion auditlog/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class AuditlogModelRegistry:
A registry that keeps track of the models that use Auditlog to track changes.
"""

DEFAULT_EXCLUDE_MODELS = ("auditlog.LogEntry", "admin.LogEntry")
DEFAULT_EXCLUDE_MODELS = (settings.AUDITLOG_LOGENTRY_MODEL, "admin.LogEntry")

def __init__(
self,
Expand Down
6 changes: 5 additions & 1 deletion auditlog_tests/test_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models

from auditlog.models import AuditlogHistoryField
from auditlog.models import AbstractLogEntry, AuditlogHistoryField
from auditlog.registry import AuditlogModelRegistry, auditlog

m2m_only_auditlog = AuditlogModelRegistry(create=False, update=False, delete=False)
Expand Down Expand Up @@ -424,6 +424,10 @@ class AutoManyRelatedModel(models.Model):
related = models.ManyToManyField(SimpleModel)


class CustomLogEntryModel(AbstractLogEntry):
pass


auditlog.register(AltPrimaryKeyModel)
auditlog.register(UUIDPrimaryKeyModel)
auditlog.register(ModelPrimaryKeyModel)
Expand Down
12 changes: 12 additions & 0 deletions auditlog_tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

import os


class DisableMigrations:

def __contains__(self, item):
return True

def __getitem__(self, item):
return None


DEBUG = True

SECRET_KEY = "test"
Expand Down Expand Up @@ -62,3 +72,5 @@
USE_TZ = True

DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

# MIGRATION_MODULES = DisableMigrations()
4 changes: 3 additions & 1 deletion auditlog_tests/test_two_step_json_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from django.test import TestCase, override_settings
from test_app.models import SimpleModel

from auditlog.models import LogEntry
from auditlog import get_logentry_model

LogEntry = get_logentry_model()


class TwoStepMigrationTest(TestCase):
Expand Down
19 changes: 16 additions & 3 deletions auditlog_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
AutoManyRelatedModel,
CharfieldTextfieldModel,
ChoicesFieldModel,
CustomLogEntryModel,
DateTimeFieldModel,
JSONModel,
ManyRelatedModel,
Expand All @@ -59,15 +60,18 @@
UUIDPrimaryKeyModel,
)

from auditlog import get_logentry_model
from auditlog.admin import LogEntryAdmin
from auditlog.cid import get_cid
from auditlog.context import disable_auditlog, set_actor
from auditlog.diff import model_instance_diff
from auditlog.middleware import AuditlogMiddleware
from auditlog.models import DEFAULT_OBJECT_REPR, LogEntry
from auditlog.models import DEFAULT_OBJECT_REPR
from auditlog.registry import AuditlogModelRegistry, AuditLogRegistrationError, auditlog
from auditlog.signals import post_log, pre_log

LogEntry = get_logentry_model()


class SimpleModelTest(TestCase):
def setUp(self):
Expand Down Expand Up @@ -1276,7 +1280,7 @@ def test_register_models_register_app(self):

self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel))
self.assertEqual(len(self.test_auditlog.get_models()), 32)
self.assertEqual(len(self.test_auditlog.get_models()), 33)

def test_register_models_register_model_with_attrs(self):
self.test_auditlog._register_models(
Expand Down Expand Up @@ -2728,7 +2732,7 @@ def post_log_receiver(sender, instance, action, error, log_entry, **_kwargs):

self.assertSignals(LogEntry.Action.DELETE)

@patch("auditlog.receivers.LogEntry.objects")
@patch("auditlog.models.LogEntry.objects")
def test_signals_errors(self, log_entry_objects_mock):
class CustomSignalError(BaseException):
pass
Expand Down Expand Up @@ -2861,3 +2865,12 @@ def test_update_public(self):
log = LogEntry.objects.get_for_object(self.public).first()
self.assertEqual(log.action, LogEntry.Action.UPDATE)
self.assertEqual(log.changes_dict["name"], ["Public", "Updated"])


class SwappableLogEntryModelTest(TestCase):

@override_settings(AUDITLOG_LOGENTRY_MODEL="test_app.CustomLogEntryModel")
def test_custom_log_entry_model(self):
self.assertEqual(get_logentry_model(), CustomLogEntryModel)
SimpleModel.objects.create(text="Hi!")
self.assertEqual(CustomLogEntryModel.objects.count(), 1)
Loading