Skip to content
Merged
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
22 changes: 22 additions & 0 deletions auditlog/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
from __future__ import annotations

from importlib.metadata import version

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

__version__ = version("django-auditlog")


def get_logentry_model():
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
4 changes: 4 additions & 0 deletions auditlog/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@

settings.AUDITLOG_MASK_CALLABLE = getattr(settings, "AUDITLOG_MASK_CALLABLE", None)

settings.AUDITLOG_LOGENTRY_MODEL = getattr(
settings, "AUDITLOG_LOGENTRY_MODEL", "auditlog.LogEntry"
)

# Use base model managers instead of default model managers
settings.AUDITLOG_USE_BASE_MANAGER = getattr(
settings, "AUDITLOG_USE_BASE_MANAGER", False
Expand Down
61 changes: 42 additions & 19 deletions auditlog/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,41 @@
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)


@contextlib.contextmanager
def set_actor(actor, remote_addr=None, remote_port=None):
"""Connect a signal receiver with current user attached."""
# Initialize thread local storage
context_data = {
"signal_duid": ("set_actor", time.time()),
"actor": actor,
"remote_addr": remote_addr,
"remote_port": remote_port,
}
return call_context_manager(context_data)


@contextlib.contextmanager
def set_extra_data(context_data):
return call_context_manager(context_data)


def call_context_manager(context_data):
"""Connect a signal receiver with current user attached."""
LogEntry = get_logentry_model()
# Initialize thread local storage
context_data["signal_duid"] = ("set_actor", time.time())
auditlog_value.set(context_data)

# Connect signal for automatic logging
set_actor = partial(
_set_actor,
user=actor,
set_extra_data = partial(
_set_extra_data,
signal_duid=context_data["signal_duid"],
)
pre_save.connect(
set_actor,
set_extra_data,
sender=LogEntry,
dispatch_uid=context_data["signal_duid"],
weak=False,
Expand All @@ -47,29 +57,42 @@ def set_actor(actor, remote_addr=None, remote_port=None):
pre_save.disconnect(sender=LogEntry, dispatch_uid=auditlog["signal_duid"])


def _set_actor(user, sender, instance, signal_duid, **kwargs):
def _set_actor(auditlog, instance, sender):
LogEntry = get_logentry_model()
auth_user_model = get_user_model()
if "actor" in auditlog:
actor = auditlog.get("actor")
if (
sender == LogEntry
and isinstance(actor, auth_user_model)
and instance.actor is None
):
instance.actor = actor
instance.actor_email = getattr(actor, "email", None)


def _set_extra_data(sender, instance, signal_duid, **kwargs):
"""Signal receiver with extra 'user' and 'signal_duid' kwargs.

This function becomes a valid signal receiver when it is curried with the actor and a dispatch id.
"""
LogEntry = get_logentry_model()
try:
auditlog = auditlog_value.get()
except LookupError:
pass
else:
if signal_duid != auditlog["signal_duid"]:
return
auth_user_model = get_user_model()
if (
sender == LogEntry
and isinstance(user, auth_user_model)
and instance.actor is None
):
instance.actor = user
instance.actor_email = getattr(user, "email", None)

instance.remote_addr = auditlog["remote_addr"]
instance.remote_port = auditlog["remote_port"]
_set_actor(auditlog, instance, sender)

for key in auditlog:
if key != "actor" and hasattr(LogEntry, key):
if callable(auditlog[key]):
setattr(instance, key, auditlog[key]())
else:
setattr(instance, key, auditlog[key])


@contextlib.contextmanager
Expand Down
5 changes: 3 additions & 2 deletions auditlog/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from django.utils.encoding import smart_str
from django.utils.module_loading import import_string

from auditlog import get_logentry_model


def track_field(field):
"""
Expand All @@ -21,7 +23,6 @@ def track_field(field):
:return: Whether the given field should be tracked.
:rtype: bool
"""
from auditlog.models import LogEntry

# Do not track many to many relations
if field.many_to_many:
Expand All @@ -30,7 +31,7 @@ def track_field(field):
# Do not track relations to LogEntry
if (
getattr(field, "remote_field", None) is not None
and field.remote_field.model == LogEntry
and field.remote_field.model == get_logentry_model()
):
return False

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
8 changes: 5 additions & 3 deletions 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 Expand Up @@ -125,8 +127,8 @@ def migrate_using_sql(self, database):
def postgres():
with connection.cursor() as cursor:
cursor.execute(
"""
UPDATE auditlog_logentry
f"""
UPDATE {LogEntry._meta.db_table}
SET changes="changes_text"::jsonb
WHERE changes_text IS NOT NULL
AND changes_text <> ''
Expand Down
17 changes: 11 additions & 6 deletions auditlog/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.contrib.auth import get_user_model

from auditlog.cid import set_cid
from auditlog.context import set_actor
from auditlog.context import set_extra_data


class AuditlogMiddleware:
Expand Down Expand Up @@ -54,12 +54,17 @@ def _get_actor(request):
return user
return None

def __call__(self, request):
remote_addr = self._get_remote_addr(request)
remote_port = self._get_remote_port(request)
user = self._get_actor(request)
def get_extra_data(self, request):
context_data = {}
context_data["remote_addr"] = self._get_remote_addr(request)
context_data["remote_port"] = self._get_remote_port(request)

context_data["actor"] = self._get_actor(request)

return context_data

def __call__(self, request):
set_cid(request)

with set_actor(actor=user, remote_addr=remote_addr, remote_port=remote_port):
with set_extra_data(context_data=self.get_extra_data(request)):
return self.get_response(request)
4 changes: 3 additions & 1 deletion auditlog/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
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.render import get_field_verbose_name, render_logentry_changes_html
from auditlog.signals import accessed

LogEntry = get_logentry_model()

MAX = 75


Expand Down
11 changes: 9 additions & 2 deletions auditlog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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 get_mask_function

DEFAULT_OBJECT_REPR = "<error forming object repr>"
Expand Down Expand Up @@ -304,7 +305,7 @@ def _mask_serialized_fields(
return data


class LogEntry(models.Model):
class AbstractLogEntry(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.
Expand Down Expand Up @@ -393,6 +394,7 @@ class Action:
objects = LogEntryManager()

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


class LogEntry(AbstractLogEntry):
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 @@ -582,7 +589,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
13 changes: 8 additions & 5 deletions auditlog/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

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, _get_manager_from_settings
from auditlog.models import _get_manager_from_settings
from auditlog.signals import post_log, pre_log


Expand Down Expand Up @@ -38,7 +39,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 @@ -58,7 +59,7 @@ def log_update(sender, instance, **kwargs):
update_fields = kwargs.get("update_fields", None)
old = _get_manager_from_settings(sender).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 @@ -77,7 +78,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 @@ -94,7 +95,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 @@ -122,6 +123,7 @@ def _create_log_entry(

if any(item[1] is False for item in pre_log_results):
return
LogEntry = get_logentry_model()

error = None
log_entry = None
Expand Down Expand Up @@ -169,6 +171,7 @@ 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"]:
return
LogEntry = get_logentry_model()

model_manager = _get_manager_from_settings(kwargs["model"])

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
Empty file.
5 changes: 5 additions & 0 deletions auditlog_tests/custom_logentry_app/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class CustomLogEntryConfig(AppConfig):
name = "custom_logentry_app"
Loading