From d417f3014283164b4a51691ed02b569daf0da799 Mon Sep 17 00:00:00 2001 From: Youngkwang Yang Date: Sat, 18 Oct 2025 00:51:53 +0900 Subject: [PATCH 1/3] Drop 'Python 3.9' support (#773) * Drop Python 3.9 support, set minimum version to 3.10 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update CHANGELOG.md * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix lint error --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 6 +++--- .pre-commit-config.yaml | 6 +++--- CHANGELOG.md | 4 ++++ auditlog/cid.py | 7 +++---- auditlog/diff.py | 8 ++++---- auditlog/middleware.py | 4 +--- auditlog/models.py | 5 +++-- auditlog/registry.py | 22 +++++++++++----------- docs/source/installation.rst | 4 ++-- setup.py | 3 +-- tox.ini | 16 +++++++--------- 12 files changed, 43 insertions(+), 44 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2246a57..6b76f23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.9' + python-version: '3.10' - name: Get pip cache dir id: pip-cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8a64fe..5bc3114 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v5 @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] services: postgres: image: postgres:15 @@ -81,7 +81,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] services: mysql: image: mysql:8.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e57ca75..8b06b16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,10 +4,10 @@ repos: rev: 25.9.0 hooks: - id: black - language_version: python3.9 + language_version: python3.10 args: - "--target-version" - - "py39" + - "py310" - repo: https://github.com/PyCQA/flake8 rev: "7.3.0" hooks: @@ -21,7 +21,7 @@ repos: rev: v3.20.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/adamchainz/django-upgrade rev: 1.29.0 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index cc735ec..c32e105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Release +#### Improvements + +- Drop 'Python 3.9' support ([#773](https://github.com/jazzband/django-auditlog/pull/773)) + ## 3.3.0 (2025-09-18) #### Improvements diff --git a/auditlog/cid.py b/auditlog/cid.py index 8d2aa9f..3e2b78f 100644 --- a/auditlog/cid.py +++ b/auditlog/cid.py @@ -1,5 +1,4 @@ from contextvars import ContextVar -from typing import Optional from django.conf import settings from django.http import HttpRequest @@ -8,7 +7,7 @@ from django.utils.module_loading import import_string correlation_id = ContextVar("auditlog_correlation_id", default=None) -def set_cid(request: Optional[HttpRequest] = None) -> None: +def set_cid(request: HttpRequest | None = None) -> None: """ A function to read the cid from a request. If the header is not in the request, then we set it to `None`. @@ -40,11 +39,11 @@ def set_cid(request: Optional[HttpRequest] = None) -> None: correlation_id.set(cid) -def _get_cid() -> Optional[str]: +def _get_cid() -> str | None: return correlation_id.get() -def get_cid() -> Optional[str]: +def get_cid() -> str | None: """ Calls the cid getter function based on `settings.AUDITLOG_CID_GETTER` diff --git a/auditlog/diff.py b/auditlog/diff.py index fc98987..72d1a76 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -1,6 +1,6 @@ import json +from collections.abc import Callable from datetime import timezone -from typing import Callable, Optional from django.conf import settings from django.core.exceptions import ObjectDoesNotExist @@ -131,7 +131,7 @@ def is_primitive(obj) -> bool: return isinstance(obj, primitive_types) -def get_mask_function(mask_callable: Optional[str] = None) -> Callable[[str], str]: +def get_mask_function(mask_callable: str | None = None) -> Callable[[str], str]: """ Get the masking function to use based on the following priority: 1. Model-specific mask_callable if provided @@ -168,8 +168,8 @@ def mask_str(value: str) -> str: def model_instance_diff( - old: Optional[Model], - new: Optional[Model], + old: Model | None, + new: Model | None, fields_to_check=None, use_json_for_changes=False, ): diff --git a/auditlog/middleware.py b/auditlog/middleware.py index bd01da3..295448e 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,5 +1,3 @@ -from typing import Optional - from django.conf import settings from django.contrib.auth import get_user_model @@ -39,7 +37,7 @@ class AuditlogMiddleware: return remote_addr @staticmethod - def _get_remote_port(request) -> Optional[int]: + def _get_remote_port(request) -> int | None: remote_port = request.headers.get("X-Forwarded-Port", "") try: diff --git a/auditlog/models.py b/auditlog/models.py index 01b54a0..d73ecda 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -1,9 +1,10 @@ import ast import contextlib import json +from collections.abc import Callable from copy import deepcopy from datetime import timezone -from typing import Any, Callable, Union +from typing import Any from dateutil import parser from dateutil.tz import gettz @@ -534,7 +535,7 @@ class LogEntry(models.Model): return changes_display_dict def _get_changes_display_for_fk_field( - self, field: Union[models.ForeignKey, models.OneToOneField], value: Any + self, field: models.ForeignKey | models.OneToOneField, value: Any ) -> str: """ :return: A string representing a given FK value and the field to which it belongs diff --git a/auditlog/registry.py b/auditlog/registry.py index c8ca907..835acd1 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -1,7 +1,7 @@ import copy from collections import defaultdict -from collections.abc import Collection, Iterable -from typing import Any, Callable, Optional, Union +from collections.abc import Callable, Collection, Iterable +from typing import Any from django.apps import apps from django.db.models import ManyToManyField, Model @@ -38,7 +38,7 @@ class AuditlogModelRegistry: delete: bool = True, access: bool = True, m2m: bool = True, - custom: Optional[dict[ModelSignal, Callable]] = None, + custom: dict[ModelSignal, Callable] | None = None, ): from auditlog.receivers import log_access, log_create, log_delete, log_update @@ -62,14 +62,14 @@ class AuditlogModelRegistry: def register( self, model: ModelBase = None, - include_fields: Optional[list[str]] = None, - exclude_fields: Optional[list[str]] = None, - mapping_fields: Optional[dict[str, str]] = None, - mask_fields: Optional[list[str]] = None, - mask_callable: Optional[str] = None, - m2m_fields: Optional[Collection[str]] = None, + include_fields: list[str] | None = None, + exclude_fields: list[str] | None = None, + mapping_fields: dict[str, str] | None = None, + mask_fields: list[str] | None = None, + mask_callable: str | None = None, + m2m_fields: Collection[str] | None = None, serialize_data: bool = False, - serialize_kwargs: Optional[dict[str, Any]] = None, + serialize_kwargs: dict[str, Any] | None = None, serialize_auditlog_fields_only: bool = False, ): """ @@ -259,7 +259,7 @@ class AuditlogModelRegistry: ] return exclude_models - def _register_models(self, models: Iterable[Union[str, dict[str, Any]]]) -> None: + def _register_models(self, models: Iterable[str | dict[str, Any]]) -> None: models = copy.deepcopy(models) for model in models: if isinstance(model, str): diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 2a2c3c2..ab4d468 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -11,10 +11,10 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. **Requirements** -- Python 3.9 or higher +- Python 3.10 or higher - Django 4.2, 5.0, 5.1, and 5.2 -Auditlog is currently tested with Python 3.9+ and Django 4.2, 5.0, 5.1, and 5.2. The latest test report can be found +Auditlog is currently tested with Python 3.10+ and Django 4.2, 5.0, 5.1, and 5.2. The latest test report can be found at https://github.com/jazzband/django-auditlog/actions. Adding Auditlog to your Django application diff --git a/setup.py b/setup.py index 9147842..2f1cc5c 100644 --- a/setup.py +++ b/setup.py @@ -29,12 +29,11 @@ setup( description="Audit log app for Django", long_description=long_description, long_description_content_type="text/markdown", - python_requires=">=3.9", + python_requires=">=3.10", install_requires=["Django>=4.2", "python-dateutil>=2.7.0"], zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/tox.ini b/tox.ini index e804268..a123535 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] envlist = - {py39,py310,py311}-django42 + {py310,py311}-django42 {py310,py311,py312}-django50 {py310,py311,py312,py313}-django51 {py310,py311,py312,py313}-django52 {py312,py313}-djangomain - py39-docs - py39-lint - py39-checkmigrations + py310-docs + py310-lint + py310-checkmigrations [testenv] setenv = @@ -42,19 +42,18 @@ basepython = py312: python3.12 py311: python3.11 py310: python3.10 - py39: python3.9 -[testenv:py39-docs] +[testenv:py310-docs] changedir = docs/source deps = -rdocs/requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html -[testenv:py39-lint] +[testenv:py310-lint] deps = pre-commit commands = pre-commit run --all-files -[testenv:py39-checkmigrations] +[testenv:py310-checkmigrations] description = Check for missing migrations changedir = auditlog_tests deps = @@ -73,7 +72,6 @@ commands = [gh-actions] python = - 3.9: py39 3.10: py310 3.11: py311 3.12: py312 From b1b6f9f4ddb3c21d6301227dbcc78b06a010c131 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Sun, 19 Oct 2025 00:55:43 +1300 Subject: [PATCH 2/3] Add base manager setting to override default manager use (#747) (#766) * Add `AUDITLOG_USE_BASE_MANAGER` setting configuration * Adjust `LogEntry._get_changes_display_for_fk_field` behaviour for setting * Adjust `log_update` and `log_m2m_changes` in `receivers.py` for setting * Add `ModelManagerTest.test_use_base_manager_setting` * Add entry in Usage documentation * (In passing, fix a formatting issue in `usage.rst`) The `AUDITLOG_USE_BASE_MANAGER` setting has a default of `False` to maintain initial backwards compatibility with previous versions. --- CHANGELOG.md | 1 + auditlog/conf.py | 5 ++ auditlog/models.py | 17 +++- auditlog/receivers.py | 12 +-- auditlog_tests/auditlog | 0 auditlog_tests/test_app/models.py | 36 ++++++++ auditlog_tests/tests.py | 134 +++++++++++++++++++++++++++++- docs/source/usage.rst | 36 +++++++- 8 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 auditlog_tests/auditlog diff --git a/CHANGELOG.md b/CHANGELOG.md index c32e105..fead883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### Improvements +- Add `AUDITLOG_USE_BASE_MANAGER` setting to override default manager use ([#766](https://github.com/jazzband/django-auditlog/pull/766)) - Drop 'Python 3.9' support ([#773](https://github.com/jazzband/django-auditlog/pull/773)) ## 3.3.0 (2025-09-18) diff --git a/auditlog/conf.py b/auditlog/conf.py index b151b24..a2895b8 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -62,3 +62,8 @@ settings.AUDITLOG_STORE_JSON_CHANGES = getattr( ) settings.AUDITLOG_MASK_CALLABLE = getattr(settings, "AUDITLOG_MASK_CALLABLE", None) + +# Use base model managers instead of default model managers +settings.AUDITLOG_USE_BASE_MANAGER = getattr( + settings, "AUDITLOG_USE_BASE_MANAGER", False +) diff --git a/auditlog/models.py b/auditlog/models.py index d73ecda..bfde86f 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -554,7 +554,9 @@ class LogEntry(models.Model): return value # Attempt to return the string representation of the object try: - return smart_str(field.related_model._default_manager.get(pk=pk_value)) + related_model_manager = _get_manager_from_settings(field.related_model) + + return smart_str(related_model_manager.get(pk=pk_value)) # ObjectDoesNotExist will be raised if the object was deleted. except ObjectDoesNotExist: return f"Deleted '{field.related_model.__name__}' ({value})" @@ -623,3 +625,16 @@ def _changes_func() -> Callable[[LogEntry], dict]: if settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT: return json_then_text return default + + +def _get_manager_from_settings(model: type[models.Model]) -> models.Manager: + """ + Get model manager as selected by AUDITLOG_USE_BASE_MANAGER. + + - True: return model._meta.base_manager + - False: return model._meta.default_manager + """ + if settings.AUDITLOG_USE_BASE_MANAGER: + return model._meta.base_manager + else: + return model._meta.default_manager diff --git a/auditlog/receivers.py b/auditlog/receivers.py index 525675a..7c38d3c 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -4,7 +4,7 @@ from django.conf import settings from auditlog.context import auditlog_disabled from auditlog.diff import model_instance_diff -from auditlog.models import LogEntry +from auditlog.models import LogEntry, _get_manager_from_settings from auditlog.signals import post_log, pre_log @@ -56,7 +56,7 @@ def log_update(sender, instance, **kwargs): """ if not instance._state.adding and instance.pk is not None: update_fields = kwargs.get("update_fields", None) - old = sender._default_manager.filter(pk=instance.pk).first() + old = _get_manager_from_settings(sender).filter(pk=instance.pk).first() _create_log_entry( action=LogEntry.Action.UPDATE, instance=instance, @@ -170,12 +170,12 @@ def make_log_m2m_changes(field_name): if action not in ["post_add", "post_clear", "post_remove"]: return + model_manager = _get_manager_from_settings(kwargs["model"]) + if action == "post_clear": - changed_queryset = kwargs["model"]._default_manager.all() + changed_queryset = model_manager.all() else: - changed_queryset = kwargs["model"]._default_manager.filter( - pk__in=kwargs["pk_set"] - ) + changed_queryset = model_manager.filter(pk__in=kwargs["pk_set"]) if action in ["post_add"]: LogEntry.objects.log_m2m_changes( diff --git a/auditlog_tests/auditlog b/auditlog_tests/auditlog new file mode 100644 index 0000000..e69de29 diff --git a/auditlog_tests/test_app/models.py b/auditlog_tests/test_app/models.py index dd240fc..e996a25 100644 --- a/auditlog_tests/test_app/models.py +++ b/auditlog_tests/test_app/models.py @@ -430,6 +430,40 @@ class SwappedManagerModel(models.Model): objects = SecretManager() + def __str__(self): + return str(self.name) + + +@auditlog.register() +class SecretRelatedModel(RelatedModelParent): + """ + A RelatedModel, but with a foreign key to an object that could be secret. + """ + + related = models.ForeignKey( + "SwappedManagerModel", related_name="related_models", on_delete=models.CASCADE + ) + one_to_one = models.OneToOneField( + to="SwappedManagerModel", + on_delete=models.CASCADE, + related_name="reverse_one_to_one", + ) + + history = AuditlogHistoryField(delete_related=True) + + def __str__(self): + return f"SecretRelatedModel #{self.pk} -> {self.related.id}" + + +class SecretM2MModel(models.Model): + m2m_related = models.ManyToManyField( + "SwappedManagerModel", related_name="m2m_related" + ) + name = models.CharField(max_length=255) + + def __str__(self): + return str(self.name) + class AutoManyRelatedModel(models.Model): related = models.ManyToManyField(SimpleModel) @@ -459,6 +493,8 @@ auditlog.register(ManyRelatedModel.recursive.through) m2m_only_auditlog.register(ManyRelatedModel, m2m_fields={"related"}) m2m_only_auditlog.register(ModelForReusableThroughModel, m2m_fields={"related"}) m2m_only_auditlog.register(OtherModelForReusableThroughModel, m2m_fields={"related"}) +m2m_only_auditlog.register(SecretM2MModel, m2m_fields={"m2m_related"}) +m2m_only_auditlog.register(SwappedManagerModel, m2m_fields={"m2m_related"}) auditlog.register(SimpleExcludeModel, exclude_fields=["text"]) auditlog.register(SimpleMappingModel, mapping_fields={"sku": "Product No."}) auditlog.register(AdditionalDataIncludedModel) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 7c2e1cc..fb1f434 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -46,6 +46,8 @@ from test_app.models import ( ProxyModel, RelatedModel, ReusableThroughRelatedModel, + SecretM2MModel, + SecretRelatedModel, SerializeNaturalKeyRelatedModel, SerializeOnlySomeOfThisModel, SerializePrimaryKeyRelatedModel, @@ -1358,7 +1360,7 @@ class RegisterModelSettingsTest(TestCase): self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel)) self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel)) - self.assertEqual(len(self.test_auditlog.get_models()), 34) + self.assertEqual(len(self.test_auditlog.get_models()), 36) def test_register_models_register_model_with_attrs(self): self.test_auditlog._register_models( @@ -2931,6 +2933,136 @@ class ModelManagerTest(TestCase): self.assertEqual(log.changes_dict["name"], ["Public", "Updated"]) +class BaseManagerSettingTest(TestCase): + """ + If the AUDITLOG_USE_BASE_MANAGER setting is enabled, "secret" objects + should be audited as if they were public, with full access to field + values. + """ + + def test_use_base_manager_setting_update(self): + """ + Model update. The default False case is covered by test_update_secret. + """ + secret = SwappedManagerModel.objects.create(is_secret=True, name="Secret") + with override_settings(AUDITLOG_USE_BASE_MANAGER=True): + secret.name = "Updated" + secret.save() + log = LogEntry.objects.get_for_object(secret).first() + self.assertEqual(log.action, LogEntry.Action.UPDATE) + self.assertEqual(log.changes_dict["name"], ["Secret", "Updated"]) + + def test_use_base_manager_setting_related_model(self): + """ + When AUDITLOG_USE_BASE_MANAGER is enabled, related model changes that + are normally invisible to the default model manager should remain + visible and not refer to "deleted" objects. + """ + t1 = datetime.datetime(2025, 1, 1, 12, tzinfo=datetime.timezone.utc) + with ( + override_settings(AUDITLOG_USE_BASE_MANAGER=False), + freezegun.freeze_time(t1), + ): + public_one = SwappedManagerModel.objects.create(name="Public One") + secret_one = SwappedManagerModel.objects.create( + is_secret=True, name="Secret One" + ) + instance_one = SecretRelatedModel.objects.create( + one_to_one=public_one, + related=secret_one, + ) + + log_one = instance_one.history.filter(timestamp=t1).first() + self.assertIsInstance(log_one, LogEntry) + display_dict = log_one.changes_display_dict + self.assertEqual(display_dict["related"][0], "None") + self.assertEqual( + display_dict["related"][1], + f"Deleted 'SwappedManagerModel' ({secret_one.id})", + "Default manager should have no visibility of secret object", + ) + self.assertEqual(display_dict["one to one"][0], "None") + self.assertEqual(display_dict["one to one"][1], "Public One") + + t2 = t1 + datetime.timedelta(days=20) + with ( + override_settings(AUDITLOG_USE_BASE_MANAGER=True), + freezegun.freeze_time(t2), + ): + public_two = SwappedManagerModel.objects.create(name="Public Two") + secret_two = SwappedManagerModel.objects.create( + is_secret=True, name="Secret Two" + ) + instance_two = SecretRelatedModel.objects.create( + one_to_one=public_two, + related=secret_two, + ) + + log_two = instance_two.history.filter(timestamp=t2).first() + self.assertIsInstance(log_two, LogEntry) + display_dict = log_two.changes_display_dict + self.assertEqual(display_dict["related"][0], "None") + self.assertEqual( + display_dict["related"][1], + "Secret Two", + "Base manager should have full visibility of secret object", + ) + self.assertEqual(display_dict["one to one"][0], "None") + self.assertEqual(display_dict["one to one"][1], "Public Two") + + def test_use_base_manager_setting_changes(self): + """ + When AUDITLOG_USE_BASE_MANAGER is enabled, registered many-to-many model + changes that refer to an object hidden from the default model manager + should remain visible and be logged. + """ + with override_settings(AUDITLOG_USE_BASE_MANAGER=False): + obj_one = SwappedManagerModel.objects.create( + is_secret=True, name="Secret One" + ) + m2m_one = SecretM2MModel.objects.create(name="M2M One") + m2m_one.m2m_related.add(obj_one) + + self.assertIn(m2m_one, obj_one.m2m_related.all(), "Secret One sees M2M One") + self.assertNotIn( + obj_one, m2m_one.m2m_related.all(), "M2M One cannot see Secret One" + ) + self.assertEqual( + 0, + LogEntry.objects.get_for_object(m2m_one).count(), + "No update with default manager", + ) + + with override_settings(AUDITLOG_USE_BASE_MANAGER=True): + obj_two = SwappedManagerModel.objects.create( + is_secret=True, name="Secret Two" + ) + m2m_two = SecretM2MModel.objects.create(name="M2M Two") + m2m_two.m2m_related.add(obj_two) + + self.assertIn(m2m_two, obj_two.m2m_related.all(), "Secret Two sees M2M Two") + self.assertNotIn( + obj_two, m2m_two.m2m_related.all(), "M2M Two cannot see Secret Two" + ) + self.assertEqual( + 1, + LogEntry.objects.get_for_object(m2m_two).count(), + "Update logged with base manager", + ) + + log_entry = LogEntry.objects.get_for_object(m2m_two).first() + self.assertEqual( + log_entry.changes, + { + "m2m_related": { + "type": "m2m", + "operation": "add", + "objects": [smart_str(obj_two)], + } + }, + ) + + class TestMaskStr(TestCase): """Test the mask_str function that masks sensitive data.""" diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 72acfa1..bc65d8a 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -141,7 +141,7 @@ For example, to use a custom masking function:: # In your_app/utils.py def custom_mask(value: str) -> str: return "****" + value[-4:] # Only show last 4 characters - + # In your models.py auditlog.register( MyModel, @@ -270,13 +270,13 @@ It will be considered when ``AUDITLOG_DISABLE_REMOTE_ADDR`` is `True`. You can use this setting to mask specific field values in all tracked models while still logging changes. This is useful when models contain sensitive fields -like `password`, `api_key`, or `secret_token`` that should not be logged +like `password`, `api_key`, or `secret_token` that should not be logged in plain text but need to be auditable. When a masked field changes, its value will be replaced with a masked representation (e.g., `****`) in the audit log instead of storing the actual value. -This setting will be applied only when `AUDITLOG_INCLUDE_ALL_MODELS`` is `True`. +This setting will be applied only when ``AUDITLOG_INCLUDE_ALL_MODELS`` is `True`. .. code-block:: python @@ -377,6 +377,36 @@ This means that primitives such as booleans, integers, etc. will be represented .. versionadded:: 3.2.0 +**AUDITLOG_USE_BASE_MANAGER** + +This configuration variable determines whether to use `base managers +`_ for +tracked models instead of their default managers. + +This setting can be useful for applications where the default manager behaviour +hides some objects from the majority of ORM queries: + +.. code-block:: python + + class SecretManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter(is_secret=False) + + + @auditlog.register() + class SwappedManagerModel(models.Model): + is_secret = models.BooleanField(default=False) + name = models.CharField(max_length=255) + + objects = SecretManager() + +In this example, when ``AUDITLOG_USE_BASE_MANAGER`` is set to `True`, objects +with the `is_secret` field set will be made visible to Auditlog. Otherwise you +may see inaccurate data in log entries, recording changes to a seemingly +"non-existent" object with empty fields. + +.. versionadded:: 3.4.0 + Actors ------ From 8c9b7b4a6e2567bc391db86155b196d9ff1e7d1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:26:37 +0200 Subject: [PATCH 3/3] [pre-commit.ci] pre-commit autoupdate (#774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/isort: 6.1.0 → 7.0.0](https://github.com/PyCQA/isort/compare/6.1.0...7.0.0) - [github.com/asottile/pyupgrade: v3.20.0 → v3.21.0](https://github.com/asottile/pyupgrade/compare/v3.20.0...v3.21.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b06b16..916a8bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,11 +14,11 @@ repos: - id: flake8 args: ["--max-line-length", "110"] - repo: https://github.com/PyCQA/isort - rev: 6.1.0 + rev: 7.0.0 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.0 hooks: - id: pyupgrade args: [--py310-plus]