Skip to content

Commit

Permalink
Initial
Browse files Browse the repository at this point in the history
  • Loading branch information
CleitonDeLima committed Oct 14, 2024
1 parent 22dcbc4 commit c44a160
Show file tree
Hide file tree
Showing 16 changed files with 122 additions and 36 deletions.
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

Check warning on line 11 in auditlog/__init__.py

View check run for this annotation

Codecov / codecov/patch

auditlog/__init__.py#L11

Added line #L11 was not covered by tests

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

Check warning on line 25 in auditlog/__init__.py

View check run for this annotation

Codecov / codecov/patch

auditlog/__init__.py#L24-L25

Added lines #L24 - L25 were not covered by tests
"AUDITLOG_LOGENTRY_MODEL must be of the form 'app_label.model_name'"
)
except LookupError:
raise ImproperlyConfigured(

Check warning on line 29 in auditlog/__init__.py

View check run for this annotation

Codecov / codecov/patch

auditlog/__init__.py#L28-L29

Added lines #L28 - L29 were not covered by tests
"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 @@ -45,3 +45,8 @@
settings.AUDITLOG_DISABLE_REMOTE_ADDR = getattr(
settings, "AUDITLOG_DISABLE_REMOTE_ADDR", False
)

# 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):
"""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 @@ -2,7 +2,9 @@

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
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
39 changes: 23 additions & 16 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 @@ -385,6 +376,7 @@ class Action:
objects = LogEntryManager()

class Meta:
abstract = True
get_latest_by = "timestamp"
ordering = ["-timestamp"]
verbose_name = _("log entry")
Expand Down Expand Up @@ -544,6 +536,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 @@ -564,7 +571,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 Expand Up @@ -592,16 +599,16 @@ def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
changes_func = None


def _changes_func() -> Callable[[LogEntry], Dict]:
def json_then_text(instance: LogEntry) -> Dict:
def _changes_func() -> Callable[[AbstractLogEntry], Dict]:
def json_then_text(instance: AbstractLogEntry) -> Dict:
if instance.changes:
return instance.changes
elif instance.changes_text:
with contextlib.suppress(ValueError):
return json.loads(instance.changes_text)
return {}

def default(instance: LogEntry) -> Dict:
def default(instance: AbstractLogEntry) -> Dict:
return instance.changes or {}

if settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT:
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.objects.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 @@ -161,14 +161,14 @@ def log_m2m_changes(signal, action, **kwargs):
changed_queryset = kwargs["model"].objects.filter(pk__in=kwargs["pk_set"])

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 @@ -38,7 +38,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/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 @@ -356,6 +356,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 @@ -5,9 +5,11 @@
from django.core.management import CommandError, call_command
from django.test import TestCase, override_settings

from auditlog.models import LogEntry
from auditlog import get_logentry_model
from auditlog_tests.models import SimpleModel

LogEntry = get_logentry_model()


class TwoStepMigrationTest(TestCase):
def test_use_text_changes_first(self):
Expand Down
Loading

0 comments on commit c44a160

Please sign in to comment.