From 7861dae4f95eaca5191a990e56a279b8ab79b5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Wed, 29 Jun 2022 17:01:37 +0000 Subject: [PATCH 001/126] Show field values before deletion in the admin --- CHANGELOG.md | 4 ++++ auditlog/mixins.py | 2 -- auditlog_tests/tests.py | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f2b4be..5ea608bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes +#### Improvements + +- feat: Display the diff for deleted objects in the admin ([#396](https://github.com/jazzband/django-auditlog/pull/396)) + ## 2.1.0 (2022-06-27) #### Improvements diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 11057510..6b045722 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -60,8 +60,6 @@ def msg_short(self, obj): msg_short.short_description = "Changes" def msg(self, obj): - if obj.action == LogEntry.Action.DELETE: - return "" # delete changes = json.loads(obj.changes) atom_changes = {} diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 7f1c145a..0e3e53d0 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1274,9 +1274,21 @@ def _create_log_entry(self, action, changes): ) def test_changes_msg_delete(self): - log_entry = self._create_log_entry(LogEntry.Action.DELETE, {}) + log_entry = self._create_log_entry( + LogEntry.Action.DELETE, + {"field one": ["value before deletion", None], "field two": [11, None]}, + ) - self.assertEqual(self.admin.msg(log_entry), "") + self.assertEqual( + self.admin.msg(log_entry), + ( + "" + "" + "" + "" + "
#FieldFromTo
1field onevalue before deletionNone
2field two11None
" + ), + ) def test_changes_msg_create(self): log_entry = self._create_log_entry( From 95929cd5b655bce5b877d78f39cdfcddab5391cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Wed, 29 Jun 2022 17:08:50 +0000 Subject: [PATCH 002/126] Add assertions for msg_short --- auditlog_tests/tests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 0e3e53d0..a79b0db1 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1279,6 +1279,7 @@ def test_changes_msg_delete(self): {"field one": ["value before deletion", None], "field two": [11, None]}, ) + self.assertEqual(self.admin.msg_short(log_entry), "") self.assertEqual( self.admin.msg(log_entry), ( @@ -1299,6 +1300,9 @@ def test_changes_msg_create(self): }, ) + self.assertEqual( + self.admin.msg_short(log_entry), "2 changes: field two, field one" + ) self.assertEqual( self.admin.msg(log_entry), ( @@ -1319,6 +1323,9 @@ def test_changes_msg_update(self): }, ) + self.assertEqual( + self.admin.msg_short(log_entry), "2 changes: field two, field one" + ) self.assertEqual( self.admin.msg(log_entry), ( @@ -1343,6 +1350,7 @@ def test_changes_msg_m2m(self): }, ) + self.assertEqual(self.admin.msg_short(log_entry), "1 change: some_m2m_field") self.assertEqual( self.admin.msg(log_entry), ( From 93907b7a6755220a351bd80ec98f83913d91c6b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 18:11:00 +0000 Subject: [PATCH 003/126] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4078be2..f13b83d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black language_version: python3.8 From 3e044444c328394324c476ef87ab7dafebe3650f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:59:31 +0000 Subject: [PATCH 004/126] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.34.0 → v2.37.1](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.37.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f13b83d1..800ca65b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.1 hooks: - id: pyupgrade args: [--py37-plus] \ No newline at end of file From 2f914c17cef09b10f9da23429a939c01f6a40c88 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 18:05:09 +0000 Subject: [PATCH 005/126] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.37.1 → v2.37.2](https://github.com/asottile/pyupgrade/compare/v2.37.1...v2.37.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 800ca65b..6706e1dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.37.1 + rev: v2.37.2 hooks: - id: pyupgrade args: [--py37-plus] \ No newline at end of file From f68af3033d13923ead55f8c6b466fe8bf6a915fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Wed, 27 Jul 2022 21:18:29 +0300 Subject: [PATCH 006/126] Pin python-dateutil>=2.7.0 (#401) --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ea608bf..c2108181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - feat: Display the diff for deleted objects in the admin ([#396](https://github.com/jazzband/django-auditlog/pull/396)) +#### Fixes + +- fix: Pin `python-dateutil` to 2.7.0 or higher for compatibility with Python 3.10 ([#401](https://github.com/jazzband/django-auditlog/pull/401)) + ## 2.1.0 (2022-06-27) #### Improvements diff --git a/setup.py b/setup.py index 6ff15da7..03a5ce1b 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ description="Audit log app for Django", long_description=long_description, long_description_content_type="text/markdown", - install_requires=["python-dateutil>=2.6.0"], + install_requires=["python-dateutil>=2.7.0"], zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", From 68cde8ffb9559a2215572801b965d8b1301212c2 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 27 Jul 2022 20:20:30 +0200 Subject: [PATCH 007/126] Prepare release 2.1.1 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2108181..47436fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changes +## 2.1.1 (2022-07-27) + #### Improvements - feat: Display the diff for deleted objects in the admin ([#396](https://github.com/jazzband/django-auditlog/pull/396)) From a24b79af0c558c50381e752377e3003f0c53b564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Mon, 1 Aug 2022 21:41:41 +0300 Subject: [PATCH 008/126] Display timestamps in server timezone (#404) --- CHANGELOG.md | 4 ++++ auditlog/mixins.py | 3 ++- auditlog_tests/tests.py | 44 +++++++++++++++++++++++++---------------- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47436fe6..83c7c6ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes +#### Fixes + +- fix: Display `created` timestamp in server timezone ([#404](https://github.com/jazzband/django-auditlog/pull/404)) + ## 2.1.1 (2022-07-27) #### Improvements diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 6b045722..2d16e1a0 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -5,6 +5,7 @@ from django.urls.exceptions import NoReverseMatch from django.utils.html import format_html, format_html_join from django.utils.safestring import mark_safe +from django.utils.timezone import localtime from auditlog.models import LogEntry @@ -13,7 +14,7 @@ class LogEntryAdminMixin: def created(self, obj): - return obj.timestamp.strftime("%Y-%m-%d %H:%M:%S") + return localtime(obj.timestamp).strftime("%Y-%m-%d %H:%M:%S") created.short_description = "Created" diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index a79b0db1..d5f1171c 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -4,6 +4,7 @@ import warnings from unittest import mock +import freezegun from dateutil.tz import gettz from django.apps import apps from django.conf import settings @@ -1233,31 +1234,40 @@ def test_changes_display_dict_arrayfield(self): class AdminPanelTest(TestCase): - @classmethod - def setUpTestData(cls): - cls.username = "test_admin" - cls.password = User.objects.make_random_password() - cls.user, created = User.objects.get_or_create(username=cls.username) - cls.user.set_password(cls.password) - cls.user.is_staff = True - cls.user.is_superuser = True - cls.user.is_active = True - cls.user.save() - cls.obj = SimpleModel.objects.create(text="For admin logentry test") + def setUp(self): + self.user = User.objects.create_user( + username="test_admin", is_staff=True, is_superuser=True, is_active=True + ) + self.site = AdminSite() + self.admin = LogEntryAdmin(LogEntry, self.site) + with freezegun.freeze_time("2022-08-01 12:00:00Z"): + self.obj = SimpleModel.objects.create(text="For admin logentry test") def test_auditlog_admin(self): - self.client.login(username=self.username, password=self.password) + self.client.force_login(self.user) log_pk = self.obj.history.latest().pk res = self.client.get("/admin/auditlog/logentry/") - assert res.status_code == 200 + self.assertEqual(res.status_code, 200) res = self.client.get("/admin/auditlog/logentry/add/") - assert res.status_code == 403 + self.assertEqual(res.status_code, 403) res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/", follow=True) - assert res.status_code == 200 + self.assertEqual(res.status_code, 200) res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/delete/") - assert res.status_code == 200 + self.assertEqual(res.status_code, 200) res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/history/") - assert res.status_code == 200 + self.assertEqual(res.status_code, 200) + + def test_created_timezone(self): + log_entry = self.obj.history.latest() + + for tz, timestamp in [ + ("UTC", "2022-08-01 12:00:00"), + ("Asia/Tbilisi", "2022-08-01 16:00:00"), + ("America/Buenos_Aires", "2022-08-01 09:00:00"), + ("Asia/Kathmandu", "2022-08-01 17:45:00"), + ]: + with self.settings(TIME_ZONE=tz): + self.assertEqual(self.admin.created(log_entry), timestamp) class DiffMsgTest(TestCase): From 4ed056bc2f065579eb8646d760f36c9387efb2d7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 20:42:20 +0200 Subject: [PATCH 009/126] [pre-commit.ci] pre-commit autoupdate (#405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 4.0.1 → 5.0.2](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.2) - [github.com/asottile/pyupgrade: v2.37.2 → v2.37.3](https://github.com/asottile/pyupgrade/compare/v2.37.2...v2.37.3) 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 6706e1dc..ee2c5082 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - "--target-version" - "py37" - repo: https://github.com/PyCQA/flake8 - rev: "4.0.1" + rev: "5.0.2" hooks: - id: flake8 args: ["--max-line-length", "110"] @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.37.2 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py37-plus] \ No newline at end of file From 1cd7d9839d171895725521339e6d3205ad4ffa9a Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 3 Aug 2022 17:26:56 +0200 Subject: [PATCH 010/126] Confirm Django 4.1 support (#406) --- CHANGELOG.md | 1 + setup.py | 1 + tox.ini | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c7c6ee..da8f5596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ #### Improvements - feat: Display the diff for deleted objects in the admin ([#396](https://github.com/jazzband/django-auditlog/pull/396)) +- Django: Confirm Django 4.1 support ([#406](https://github.com/jazzband/django-auditlog/pull/406)) #### Fixes diff --git a/setup.py b/setup.py index 03a5ce1b..e3f1f0b3 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", "License :: OSI Approved :: MIT License", ], ) diff --git a/tox.ini b/tox.ini index ec46ab62..e7fc0984 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = {py37,py38,py39,py310}-django32 - {py38,py39,py310}-django{40,main} + {py38,py39,py310}-django{40,41,main} py37-docs py38-lint @@ -14,6 +14,7 @@ commands = deps = django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 djangomain: https://github.com/django/django/archive/main.tar.gz # Test requirements coverage From 830152f0f45337f716368e47a453276f0b3c4490 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 4 Aug 2022 22:03:25 +0200 Subject: [PATCH 011/126] Fix code block in usage.rst --- docs/source/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index eaa3ed3b..0fb0f384 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -19,7 +19,7 @@ Automatically logging changes Auditlog can automatically log changes to objects for you. This functionality is based on Django's signals, but linking your models to Auditlog is even easier than using signals. -Registering your model for logging can be done with a single line of code, as the following example illustrates:: +Registering your model for logging can be done with a single line of code, as the following example illustrates: .. code-block:: python From eb5e87308225f14753b2e8c32661e83507ac64d2 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 8 Aug 2022 11:53:03 +0200 Subject: [PATCH 012/126] Add django-upgrade to pre-commit-config.yaml (#411) --- .pre-commit-config.yaml | 7 ++++++- auditlog/middleware.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee2c5082..52f3073d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,4 +21,9 @@ repos: rev: v2.37.3 hooks: - id: pyupgrade - args: [--py37-plus] \ No newline at end of file + args: [--py37-plus] + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.7.0 + hooks: + - id: django-upgrade + args: [--target-version, "3.2"] \ No newline at end of file diff --git a/auditlog/middleware.py b/auditlog/middleware.py index aa7b1236..803f9736 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -14,9 +14,9 @@ def __init__(self, get_response=None): def __call__(self, request): - if request.META.get("HTTP_X_FORWARDED_FOR"): + if request.headers.get("X-Forwarded-For"): # In case of proxy, set 'original' address - remote_addr = request.META.get("HTTP_X_FORWARDED_FOR").split(",")[0] + remote_addr = request.headers.get("X-Forwarded-For").split(",")[0] else: remote_addr = request.META.get("REMOTE_ADDR") From a00d2c227fad52e06d865037406d5483d0616b77 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 20:24:17 +0200 Subject: [PATCH 013/126] [pre-commit.ci] pre-commit autoupdate (#413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 5.0.2 → 5.0.4](https://github.com/PyCQA/flake8/compare/5.0.2...5.0.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 52f3073d..51b0c6df 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - "--target-version" - "py37" - repo: https://github.com/PyCQA/flake8 - rev: "5.0.2" + rev: "5.0.4" hooks: - id: flake8 args: ["--max-line-length", "110"] From c13d6ec88dc62b5f35e2ac73576f6d8bdd071d8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Aug 2022 20:03:15 +0200 Subject: [PATCH 014/126] [pre-commit.ci] pre-commit autoupdate (#416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/adamchainz/django-upgrade: 1.7.0 → 1.8.0](https://github.com/adamchainz/django-upgrade/compare/1.7.0...1.8.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51b0c6df..3e7c2efb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.7.0 + rev: 1.8.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] \ No newline at end of file From 18868aaaed06131ef927e951940e654b1e20cdcd Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Tue, 16 Aug 2022 23:29:45 +0200 Subject: [PATCH 015/126] Handle port in `remote_addr` --- CHANGELOG.md | 1 + auditlog/middleware.py | 11 ++++++++--- auditlog_tests/tests.py | 13 +++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da8f5596..f28da62b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Fixes - fix: Display `created` timestamp in server timezone ([#404](https://github.com/jazzband/django-auditlog/pull/404)) +- fix: Handle port in `remote_addr` ([#417](https://github.com/jazzband/django-auditlog/pull/417)) ## 2.1.1 (2022-07-27) diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 803f9736..19875c9c 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -12,13 +12,18 @@ class AuditlogMiddleware: def __init__(self, get_response=None): self.get_response = get_response - def __call__(self, request): - + @staticmethod + def _get_remote_addr(request): if request.headers.get("X-Forwarded-For"): # In case of proxy, set 'original' address remote_addr = request.headers.get("X-Forwarded-For").split(",")[0] + # Remove port number from remote_addr + return remote_addr.split(":")[0] else: - remote_addr = request.META.get("REMOTE_ADDR") + return request.META.get("REMOTE_ADDR") + + def __call__(self, request): + remote_addr = self._get_remote_addr(request) if hasattr(request, "user") and request.user.is_authenticated: context = set_actor(actor=request.user, remote_addr=remote_addr) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index d5f1171c..d2b84f68 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -437,6 +437,19 @@ def test_exception(self): self.assert_no_listeners() + def test_get_remote_addr(self): + tests = [ # (headers, expected_remote_addr) + ({}, "127.0.0.1"), + ({"HTTP_X_FORWARDED_FOR": "127.0.0.2"}, "127.0.0.2"), + ({"HTTP_X_FORWARDED_FOR": "127.0.0.3:1234"}, "127.0.0.3"), + ] + for headers, expected_remote_addr in tests: + with self.subTest(headers=headers): + request = self.factory.get("/", **headers) + self.assertEqual( + self.middleware._get_remote_addr(request), expected_remote_addr + ) + class SimpleIncludeModelTest(TestCase): """Log only changes in include_fields""" From 174605d65012b260afd71663669bb65a61b92c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Magimel?= Date: Wed, 17 Aug 2022 18:51:14 +0200 Subject: [PATCH 016/126] docs(readme): update the release section (#351) Co-authored-by: Hasan Ramezani --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 87d6a651..ffae6b1d 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,10 @@ If you have great ideas for Auditlog, or if you like to improve something, feel Releases -------- -1. Make sure all tests on `master` are green. -2. Create a new branch `vX.Y.Z` from master for that specific release. -3. Bump versions in `setup.py` and `docs/source/conf.py` (docs have 2 places where the versions need to be changed!) -4. Pull request `vX.Y.Z` -> `master`. -5. Pull request `master` -> `stable`. This merge triggers the deploy to pypi. +1. Make sure all tests on `master` are green +2. Create a new branch `vX.Y.Z` from master for that specific release +3. Update the CHANGELOG release date +4. Pull request `vX.Y.Z` -> `master` +5. As a project lead, once the PR is merged, create and push a tag `vX.Y.Z`: this will trigger the release build and a notification will be sent from Jazzband of the availability of two packages (tgz and wheel) +6. Test the install +7. Publish the release to PyPI From 57423fcb3a1d3785a5b7ea6992cd6a5edd46ae9f Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 18 Aug 2022 13:06:33 +0200 Subject: [PATCH 017/126] Add required Python and Django version to setup.py (#419) --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e3f1f0b3..9135b245 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,8 @@ description="Audit log app for Django", long_description=long_description, long_description_content_type="text/markdown", - install_requires=["python-dateutil>=2.7.0"], + python_requires=">=3.7", + install_requires=["Django>=3.2", "python-dateutil>=2.7.0"], zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", From 777bd537e7d0a41a0f5baa562f2a3cbe20080bae Mon Sep 17 00:00:00 2001 From: August Raack <60975983+sum-rock@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:45:50 -0500 Subject: [PATCH 018/126] Add serialized object field (#412) --- CHANGELOG.md | 4 + .../0011_logentry_serialized_data.py | 18 ++ auditlog/models.py | 82 ++++++ auditlog/registry.py | 31 ++- auditlog_tests/models.py | 55 ++++ auditlog_tests/tests.py | 241 +++++++++++++++++- docs/source/usage.rst | 32 +++ 7 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 auditlog/migrations/0011_logentry_serialized_data.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f28da62b..594eaeba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes +#### Improvements + +- feat: Add `serialized_data` field on `LogEntry` model. ([#412](https://github.com/jazzband/django-auditlog/pull/412)) + #### Fixes - fix: Display `created` timestamp in server timezone ([#404](https://github.com/jazzband/django-auditlog/pull/404)) diff --git a/auditlog/migrations/0011_logentry_serialized_data.py b/auditlog/migrations/0011_logentry_serialized_data.py new file mode 100644 index 00000000..39b9d65a --- /dev/null +++ b/auditlog/migrations/0011_logentry_serialized_data.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0 on 2022-08-05 19:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("auditlog", "0010_alter_logentry_timestamp"), + ] + + operations = [ + migrations.AddField( + model_name="logentry", + name="serialized_data", + field=models.JSONField(null=True), + ), + ] diff --git a/auditlog/models.py b/auditlog/models.py index 537c21be..63c154f4 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -1,11 +1,14 @@ import ast import json +from copy import deepcopy +from typing import Any, Dict, List from dateutil import parser from dateutil.tz import gettz from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType +from django.core import serializers from django.core.exceptions import FieldDoesNotExist from django.db import DEFAULT_DB_ALIAS, models from django.db.models import Q, QuerySet @@ -13,6 +16,8 @@ from django.utils.encoding import smart_str from django.utils.translation import gettext_lazy as _ +from auditlog.diff import mask_str + class LogEntryManager(models.Manager): """ @@ -39,6 +44,9 @@ def log_create(self, instance, **kwargs): ) kwargs.setdefault("object_pk", pk) kwargs.setdefault("object_repr", smart_str(instance)) + kwargs.setdefault( + "serialized_data", self._get_serialized_data_or_none(instance) + ) if isinstance(pk, int): kwargs.setdefault("object_id", pk) @@ -208,6 +216,79 @@ def _get_pk_value(self, instance): pk = self._get_pk_value(pk) return pk + def _get_serialized_data_or_none(self, instance): + from auditlog.registry import auditlog + + opts = auditlog.get_serialize_options(instance.__class__) + if not opts["serialize_data"]: + return None + + model_fields = auditlog.get_model_fields(instance.__class__) + kwargs = opts.get("serialize_kwargs", {}) + + if opts["serialize_auditlog_fields_only"]: + kwargs.setdefault( + "fields", self._get_applicable_model_fields(instance, model_fields) + ) + + instance_copy = self._get_copy_with_python_typed_fields(instance) + data = dict( + json.loads(serializers.serialize("json", (instance_copy,), **kwargs))[0] + ) + + mask_fields = model_fields["mask_fields"] + if mask_fields: + data = self._mask_serialized_fields(data, mask_fields) + + return data + + def _get_copy_with_python_typed_fields(self, instance): + """ + Attempt to create copy of instance and coerce types on instance fields + + The Django core serializer assumes that the values on object fields are + correctly typed to their respective fields. Updates made to an object's + in-memory state may not meet this assumption. To prevent this violation, values + are typed by calling `to_python` from the field object, the result is set on a + copy of the instance and the copy is sent to the serializer. + """ + try: + instance_copy = deepcopy(instance) + except TypeError: + instance_copy = instance + for field in instance_copy._meta.fields: + if not field.is_relation: + value = getattr(instance_copy, field.name) + setattr(instance_copy, field.name, field.to_python(value)) + return instance_copy + + def _get_applicable_model_fields( + self, instance, model_fields: Dict[str, List[str]] + ) -> List[str]: + include_fields = model_fields["include_fields"] + exclude_fields = model_fields["exclude_fields"] + all_field_names = [field.name for field in instance._meta.fields] + + if not include_fields and not exclude_fields: + return all_field_names + + return list(set(include_fields or all_field_names).difference(exclude_fields)) + + def _mask_serialized_fields( + self, data: Dict[str, Any], mask_fields: List[str] + ) -> Dict[str, Any]: + all_field_data = data.pop("fields") + + masked_field_data = {} + for key, value in all_field_data.items(): + if isinstance(value, str) and key in mask_fields: + masked_field_data[key] = mask_str(value) + else: + masked_field_data[key] = value + + data["fields"] = masked_field_data + return data + class LogEntry(models.Model): """ @@ -253,6 +334,7 @@ class Action: blank=True, db_index=True, null=True, verbose_name=_("object id") ) object_repr = models.TextField(verbose_name=_("object representation")) + serialized_data = models.JSONField(null=True) action = models.PositiveSmallIntegerField( choices=Action.choices, verbose_name=_("action"), db_index=True ) diff --git a/auditlog/registry.py b/auditlog/registry.py index 92752517..d2ab0b0f 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -28,6 +28,10 @@ DispatchUID = Tuple[int, int, int] +class AuditLogRegistrationError(Exception): + pass + + class AuditlogModelRegistry: """ A registry that keeps track of the models that use Auditlog to track changes. @@ -68,6 +72,9 @@ def register( mapping_fields: Optional[Dict[str, str]] = None, mask_fields: Optional[List[str]] = None, m2m_fields: Optional[Collection[str]] = None, + serialize_data: bool = False, + serialize_kwargs: Optional[Dict[str, Any]] = None, + serialize_auditlog_fields_only: bool = False, ): """ Register a model with auditlog. Auditlog will then track mutations on this model's instances. @@ -78,7 +85,9 @@ def register( :param mapping_fields: Mapping from field names to strings in diff. :param mask_fields: The fields to mask for sensitive info. :param m2m_fields: The fields to handle as many to many. - + :param serialize_data: Option to include a dictionary of the objects state in the auditlog. + :param serialize_kwargs: Optional kwargs to pass to Django serializer + :param serialize_auditlog_fields_only: Only fields being considered in changes will be serialized. """ if include_fields is None: @@ -91,6 +100,14 @@ def register( mask_fields = [] if m2m_fields is None: m2m_fields = set() + if serialize_kwargs is None: + serialize_kwargs = {} + + if (serialize_kwargs or serialize_auditlog_fields_only) and not serialize_data: + raise AuditLogRegistrationError( + "Serializer options were given but the 'serialize_data' option is not " + "set. Did you forget to set serialized_data to True?" + ) def registrar(cls): """Register models for a given class.""" @@ -103,6 +120,9 @@ def registrar(cls): "mapping_fields": mapping_fields, "mask_fields": mask_fields, "m2m_fields": m2m_fields, + "serialize_data": serialize_data, + "serialize_kwargs": serialize_kwargs, + "serialize_auditlog_fields_only": serialize_auditlog_fields_only, } self._connect_signals(cls) @@ -153,6 +173,15 @@ def get_model_fields(self, model: ModelBase): "mask_fields": list(self._registry[model]["mask_fields"]), } + def get_serialize_options(self, model: ModelBase): + return { + "serialize_data": bool(self._registry[model]["serialize_data"]), + "serialize_kwargs": dict(self._registry[model]["serialize_kwargs"]), + "serialize_auditlog_fields_only": bool( + self._registry[model]["serialize_auditlog_fields_only"] + ), + } + def _connect_signals(self, model): """ Connect signals for the model. diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index 0e6af497..1a6c5a22 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -262,6 +262,44 @@ class JSONModel(models.Model): history = AuditlogHistoryField(delete_related=False) +class SerializeThisModel(models.Model): + label = models.CharField(max_length=24, unique=True) + timestamp = models.DateTimeField() + nullable = models.IntegerField(null=True) + nested = models.JSONField() + mask_me = models.CharField(max_length=255, null=True) + code = models.UUIDField(null=True) + date = models.DateField(null=True) + + history = AuditlogHistoryField(delete_related=False) + + def natural_key(self): + return self.label + + +class SerializeOnlySomeOfThisModel(models.Model): + this = models.CharField(max_length=24) + not_this = models.CharField(max_length=24) + + history = AuditlogHistoryField(delete_related=False) + + +class SerializePrimaryKeyRelatedModel(models.Model): + serialize_this = models.ForeignKey(to=SerializeThisModel, on_delete=models.CASCADE) + subheading = models.CharField(max_length=255) + value = models.IntegerField() + + history = AuditlogHistoryField(delete_related=False) + + +class SerializeNaturalKeyRelatedModel(models.Model): + serialize_this = models.ForeignKey(to=SerializeThisModel, on_delete=models.CASCADE) + subheading = models.CharField(max_length=255) + value = models.IntegerField() + + history = AuditlogHistoryField(delete_related=False) + + auditlog.register(AltPrimaryKeyModel) auditlog.register(UUIDPrimaryKeyModel) auditlog.register(ProxyModel) @@ -278,3 +316,20 @@ class JSONModel(models.Model): auditlog.register(PostgresArrayFieldModel) auditlog.register(NoDeleteHistoryModel) auditlog.register(JSONModel) +auditlog.register( + SerializeThisModel, + serialize_data=True, + mask_fields=["mask_me"], +) +auditlog.register( + SerializeOnlySomeOfThisModel, + serialize_data=True, + serialize_auditlog_fields_only=True, + exclude_fields=["not_this"], +) +auditlog.register(SerializePrimaryKeyRelatedModel, serialize_data=True) +auditlog.register( + SerializeNaturalKeyRelatedModel, + serialize_data=True, + serialize_kwargs={"use_natural_foreign_keys": True}, +) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index d2b84f68..0cb1e7a8 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -21,7 +21,7 @@ from auditlog.diff import model_instance_diff from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry -from auditlog.registry import AuditlogModelRegistry, auditlog +from auditlog.registry import AuditlogModelRegistry, AuditLogRegistrationError, auditlog from auditlog_tests.models import ( AdditionalDataIncludedModel, AltPrimaryKeyModel, @@ -35,6 +35,10 @@ PostgresArrayFieldModel, ProxyModel, RelatedModel, + SerializeNaturalKeyRelatedModel, + SerializeOnlySomeOfThisModel, + SerializePrimaryKeyRelatedModel, + SerializeThisModel, SimpleExcludeModel, SimpleIncludeModel, SimpleMappingModel, @@ -1000,7 +1004,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()), 19) + self.assertEqual(len(self.test_auditlog.get_models()), 23) def test_register_models_register_model_with_attrs(self): self.test_auditlog._register_models( @@ -1117,6 +1121,17 @@ def test_register_from_settings_register_models(self): self.assertEqual(fields["include_fields"], ["label"]) self.assertEqual(fields["exclude_fields"], ["text"]) + def test_registration_error_if_bad_serialize_params(self): + with self.assertRaisesMessage( + AuditLogRegistrationError, + "Serializer options were given but the 'serialize_data' option is not " + "set. Did you forget to set serialized_data to True?", + ): + register = AuditlogModelRegistry() + register.register( + SimpleModel, serialize_kwargs={"fields": ["text", "integer"]} + ) + class ChoicesFieldModelTest(TestCase): def setUp(self): @@ -1534,3 +1549,225 @@ def test_when_field_doesnt_exist(self): {"boolean": ("True", "False")}, msg="ObjectDoesNotExist should be handled", ) + + +class TestModelSerialization(TestCase): + def setUp(self): + super().setUp() + self.test_date = datetime.datetime(2022, 1, 1, 12, tzinfo=datetime.timezone.utc) + self.test_date_string = datetime.datetime.strftime( + self.test_date, "%Y-%m-%dT%XZ" + ) + + def test_does_not_serialize_data_when_not_configured(self): + instance = SimpleModel.objects.create( + text="sample text here", boolean=True, integer=4 + ) + + log = instance.history.first() + self.assertIsNone(log.serialized_data) + + def test_serializes_data_on_create(self): + with freezegun.freeze_time(self.test_date): + instance = SerializeThisModel.objects.create( + label="test label", + timestamp=self.test_date, + nullable=4, + nested={"foo": True, "bar": False}, + ) + + log = instance.history.first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 0) + self.assertDictEqual( + log.serialized_data["fields"], + { + "label": "test label", + "timestamp": self.test_date_string, + "nullable": 4, + "nested": {"foo": True, "bar": False}, + "mask_me": None, + "date": None, + "code": None, + }, + ) + + def test_serializes_data_on_update(self): + with freezegun.freeze_time(self.test_date): + instance = SerializeThisModel.objects.create( + label="test label", + timestamp=self.test_date, + nullable=4, + nested={"foo": True, "bar": False}, + ) + + update_date = self.test_date + datetime.timedelta(days=4) + with freezegun.freeze_time(update_date): + instance.label = "test label change" + instance.save() + + log = instance.history.filter(timestamp=update_date).first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 1) + self.assertDictEqual( + log.serialized_data["fields"], + { + "label": "test label change", + "timestamp": self.test_date_string, + "nullable": 4, + "nested": {"foo": True, "bar": False}, + "mask_me": None, + "date": None, + "code": None, + }, + ) + + def test_serializes_data_on_delete(self): + with freezegun.freeze_time(self.test_date): + instance = SerializeThisModel.objects.create( + label="test label", + timestamp=self.test_date, + nullable=4, + nested={"foo": True, "bar": False}, + ) + + obj_id = int(instance.id) + delete_date = self.test_date + datetime.timedelta(days=4) + with freezegun.freeze_time(delete_date): + instance.delete() + + log = LogEntry.objects.filter(object_id=obj_id, timestamp=delete_date).first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 2) + self.assertDictEqual( + log.serialized_data["fields"], + { + "label": "test label", + "timestamp": self.test_date_string, + "nullable": 4, + "nested": {"foo": True, "bar": False}, + "mask_me": None, + "date": None, + "code": None, + }, + ) + + def test_serialize_string_representations(self): + with freezegun.freeze_time(self.test_date): + instance = SerializeThisModel.objects.create( + label="test label", + nullable=4, + nested={"foo": 10, "bar": False}, + timestamp="2022-03-01T12:00Z", + date="2022-04-05", + code="e82d5e53-ca80-4037-af55-b90752326460", + ) + + log = instance.history.first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 0) + self.assertDictEqual( + log.serialized_data["fields"], + { + "label": "test label", + "timestamp": "2022-03-01T12:00:00Z", + "date": "2022-04-05", + "code": "e82d5e53-ca80-4037-af55-b90752326460", + "nullable": 4, + "nested": {"foo": 10, "bar": False}, + "mask_me": None, + }, + ) + + def test_serialize_mask_fields(self): + with freezegun.freeze_time(self.test_date): + instance = SerializeThisModel.objects.create( + label="test label", + nullable=4, + timestamp=self.test_date, + nested={"foo": 10, "bar": False}, + mask_me="confidential", + ) + + log = instance.history.first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 0) + self.assertDictEqual( + log.serialized_data["fields"], + { + "label": "test label", + "timestamp": self.test_date_string, + "nullable": 4, + "nested": {"foo": 10, "bar": False}, + "mask_me": "******ential", + "date": None, + "code": None, + }, + ) + + def test_serialize_only_auditlog_fields(self): + with freezegun.freeze_time(self.test_date): + instance = SerializeOnlySomeOfThisModel.objects.create( + this="this should be there", not_this="leave this bit out" + ) + + log = instance.history.first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 0) + self.assertDictEqual( + log.serialized_data["fields"], {"this": "this should be there"} + ) + self.assertDictEqual( + log.changes_dict, + {"this": ["None", "this should be there"], "id": ["None", "1"]}, + ) + + def test_serialize_related(self): + with freezegun.freeze_time(self.test_date): + serialize_this = SerializeThisModel.objects.create( + label="test label", + nested={"foo": "bar"}, + timestamp=self.test_date, + ) + instance = SerializePrimaryKeyRelatedModel.objects.create( + serialize_this=serialize_this, + subheading="use a primary key for this serialization, please.", + value=10, + ) + + log = instance.history.first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 0) + self.assertDictEqual( + log.serialized_data["fields"], + { + "serialize_this": serialize_this.id, + "subheading": "use a primary key for this serialization, please.", + "value": 10, + }, + ) + + def test_serialize_related_with_kwargs(self): + with freezegun.freeze_time(self.test_date): + serialize_this = SerializeThisModel.objects.create( + label="test label", + nested={"foo": "bar"}, + timestamp=self.test_date, + ) + instance = SerializeNaturalKeyRelatedModel.objects.create( + serialize_this=serialize_this, + subheading="use a natural key for this serialization, please.", + value=11, + ) + + log = instance.history.first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 0) + self.assertDictEqual( + log.serialized_data["fields"], + { + "serialize_this": "test label", + "subheading": "use a natural key for this serialization, please.", + "value": 11, + }, + ) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 0fb0f384..00abffd6 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -115,6 +115,38 @@ Note that when the user changes multiple many-to-many fields on the same object .. versionadded:: 2.1.0 +**Serialized Data** + +The state of an object following a change action may be optionally serialized and persisted in the ``LogEntry.serialized_data`` JSONField. To enable this feature for a registered model, add ``serialize_data=True`` to the kwargs on the ``auditlog.register(...)`` method. Object serialization will not occur unless this kwarg is set. + +.. code-block:: python + + auditlog.register(MyModel, serialize_data=True) + +Objects are serialized using the Django core serializer. Keyword arguments may be passed to the serializer through ``serialize_kwargs``. + +.. code-block:: python + + auditlog.register( + MyModel, + serialize_data=True, + serialize_kwargs={"fields": ["foo", "bar", "biz", "baz"]} + ) + +Note that all fields on the object will be serialized unless restricted with one or more configurations. The `serialize_kwargs` option contains a `fields` argument and this may be given an inclusive list of field names to serialize (as shown above). Alternatively, one may set ``serialize_auditlog_fields_only`` to ``True`` when registering a model with ``exclude_fields`` and ``include_fields`` set (as shown below). This will cause the data persisted in ``LogEntry.serialized_data`` to be limited to the same scope that is persisted within the ``LogEntry.changes`` field. + +.. code-block:: python + + auditlog.register( + MyModel, + exclude_fields=["ssn", "confidential"] + serialize_data=True, + serialize_auditlog_fields_only=True + ) + +Field masking is supported in object serialization. Any value belonging to a field whose name is found in the ``mask_fields`` list will be masked in the serialized object data. Masked values are obfuscated with asterisks in the same way as they are in the ``LogEntry.changes`` field. + + Settings -------- From 527f870034f37cbe9116af8e30c118cac5be2b14 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Aug 2022 20:26:04 +0200 Subject: [PATCH 019/126] [pre-commit.ci] pre-commit autoupdate (#422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/adamchainz/django-upgrade: 1.8.0 → 1.9.0](https://github.com/adamchainz/django-upgrade/compare/1.8.0...1.9.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e7c2efb..ea576376 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.8.0 + rev: 1.9.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] \ No newline at end of file From 93602bd210cb1c6cb3ab26a733d4662bc0443a74 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 20:31:07 +0200 Subject: [PATCH 020/126] [pre-commit.ci] pre-commit autoupdate (#423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea576376..e3379918 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black language_version: python3.8 From acada9edf95500430e29fb286910c2d5921c81e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Sep 2022 20:34:11 +0200 Subject: [PATCH 021/126] [pre-commit.ci] pre-commit autoupdate (#425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/adamchainz/django-upgrade: 1.9.0 → 1.10.0](https://github.com/adamchainz/django-upgrade/compare/1.9.0...1.10.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3379918..de429901 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.9.0 + rev: 1.10.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] \ No newline at end of file From fb90112c50b8547d9bd47c5fe5d5a3e0097a4930 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 21:47:54 +0200 Subject: [PATCH 022/126] [pre-commit.ci] pre-commit autoupdate (#430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.37.3 → v2.38.0](https://github.com/asottile/pyupgrade/compare/v2.37.3...v2.38.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de429901..dd35a223 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.0 hooks: - id: pyupgrade args: [--py37-plus] From d74c118834ac9f4f5dd76fd443fe905491fcd976 Mon Sep 17 00:00:00 2001 From: Mathieu Rampant Date: Tue, 20 Sep 2022 16:07:41 -0400 Subject: [PATCH 023/126] Added verbose field name in admin (#428) Co-authored-by: Hasan Ramezani --- CHANGELOG.md | 1 + auditlog/mixins.py | 25 +++++++++++++++++++++++-- auditlog_tests/tests.py | 40 +++++++++++++++++++++++++++++++++------- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 594eaeba..66afb7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Improvements - 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) #### Fixes diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 2d16e1a0..1936763a 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -2,12 +2,15 @@ from django import urls as urlresolvers from django.conf import settings +from django.core.exceptions import FieldDoesNotExist +from django.forms.utils import pretty_name from django.urls.exceptions import NoReverseMatch from django.utils.html import format_html, format_html_join from django.utils.safestring import mark_safe from django.utils.timezone import localtime from auditlog.models import LogEntry +from auditlog.registry import auditlog MAX = 75 @@ -81,7 +84,9 @@ def msg(self, obj): msg.append("") msg.append(self._format_header("#", "Field", "From", "To")) for i, (field, change) in enumerate(sorted(atom_changes.items()), 1): - value = [i, field] + (["***", "***"] if field == "password" else change) + value = [i, self.field_verbose_name(obj, field)] + ( + ["***", "***"] if field == "password" else change + ) msg.append(self._format_line(*value)) msg.append("
") @@ -99,7 +104,7 @@ def msg(self, obj): format_html( "{}{}{}{}", i, - field, + self.field_verbose_name(obj, field), change["operation"], change_html, ) @@ -120,3 +125,19 @@ def _format_line(self, *values): return format_html( "".join(["", "{}" * len(values), ""]), *values ) + + def field_verbose_name(self, obj, field_name: str): + model = obj.content_type.model_class() + try: + model_fields = auditlog.get_model_fields(model._meta.model) + mapping_field_name = model_fields["mapping_fields"].get(field_name) + if mapping_field_name: + return mapping_field_name + except KeyError: + # Model definition in auditlog was probably removed + pass + try: + field = model._meta.get_field(field_name) + return pretty_name(getattr(field, "verbose_name", field_name)) + except FieldDoesNotExist: + return pretty_name(field_name) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 0cb1e7a8..5751f4f4 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1323,8 +1323,8 @@ def test_changes_msg_delete(self): ( "" "" - "" - "" + "" + "" "
#FieldFromTo
1field onevalue before deletionNone
2field two11None
1Field onevalue before deletionNone
2Field two11None
" ), ) @@ -1346,8 +1346,8 @@ def test_changes_msg_create(self): ( "" "" - "" - "" + "" + "" "
#FieldFromTo
1field oneNonea value
2field twoNone11
1Field oneNonea value
2Field twoNone11
" ), ) @@ -1369,9 +1369,9 @@ def test_changes_msg_update(self): ( "" "" - "" + "" "" - "" + "" "
#FieldFromTo
1field oneold value of field one
1Field oneold value of field onenew value of field one
2field two1142
2Field two1142
" ), ) @@ -1394,12 +1394,38 @@ def test_changes_msg_m2m(self): ( "" "" - "" "
#RelationshipActionObjects
1some_m2m_fieldaddExample User (user 1)" + "
1Some m2m fieldaddExample User (user 1)" "
Illustration (user 42)
" ), ) + def test_unregister_after_log(self): + log_entry = self._create_log_entry( + LogEntry.Action.CREATE, + { + "field two": [None, 11], + "field one": [None, "a value"], + }, + ) + # Unregister + auditlog.unregister(SimpleModel) + self.assertEqual( + self.admin.msg_short(log_entry), "2 changes: field two, field one" + ) + self.assertEqual( + self.admin.msg(log_entry), + ( + "" + "" + "" + "" + "
#FieldFromTo
1Field oneNonea value
2Field twoNone11
" + ), + ) + # Re-register + auditlog.register(SimpleModel) + class NoDeleteHistoryTest(TestCase): def test_delete_related(self): From a56d0e6f784f3d0e1107f41c8c7a3f2423686c14 Mon Sep 17 00:00:00 2001 From: Rahul Prasad Date: Wed, 21 Sep 2022 12:23:10 +0530 Subject: [PATCH 024/126] added fix for OneToOneRel error happening in case of Polymorphic model (#429) --- CHANGELOG.md | 1 + auditlog/diff.py | 6 +++++- auditlog_tests/tests.py | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66afb7e4..b84bfc7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - fix: Display `created` timestamp in server timezone ([#404](https://github.com/jazzband/django-auditlog/pull/404)) - fix: Handle port in `remote_addr` ([#417](https://github.com/jazzband/django-auditlog/pull/417)) +- fix: Handle the error with AttributeError: 'OneToOneRel' error occur during a `PolymorphicModel` has relation with other models ([#429](https://github.com/jazzband/django-auditlog/pull/429)) ## 2.1.1 (2022-07-27) diff --git a/auditlog/diff.py b/auditlog/diff.py index 83cd1aab..74bd9a02 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -70,7 +70,11 @@ def get_field_value(obj, field): else: value = smart_str(getattr(obj, field.name, None)) except ObjectDoesNotExist: - value = field.default if field.default is not NOT_PROVIDED else None + value = ( + field.default + if getattr(field, "default", NOT_PROVIDED) is not NOT_PROVIDED + else None + ) return value diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 5751f4f4..0adb7105 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1145,7 +1145,6 @@ def setUp(self): ) def test_changes_display_dict_single_choice(self): - self.assertEqual( self.obj.history.latest().changes_display_dict["status"][1], "Red", From 0f575250588047b747cdfcad4b8a56be1ff16bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Wed, 21 Sep 2022 14:19:49 +0000 Subject: [PATCH 025/126] Support search by custom USERNAME_FIELD (#432) --- CHANGELOG.md | 1 + auditlog/admin.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b84bfc7d..ca6a8722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - fix: Display `created` timestamp in server timezone ([#404](https://github.com/jazzband/django-auditlog/pull/404)) - fix: Handle port in `remote_addr` ([#417](https://github.com/jazzband/django-auditlog/pull/417)) - fix: Handle the error with AttributeError: 'OneToOneRel' error occur during a `PolymorphicModel` has relation with other models ([#429](https://github.com/jazzband/django-auditlog/pull/429)) +- fix: Support search by custom USERNAME_FIELD ([#432](https://github.com/jazzband/django-auditlog/pull/432)) ## 2.1.1 (2022-07-27) diff --git a/auditlog/admin.py b/auditlog/admin.py index 2d796753..ee868e2a 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.contrib.auth import get_user_model from auditlog.filters import ResourceTypeFilter from auditlog.mixins import LogEntryAdminMixin @@ -14,7 +15,7 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): "changes", "actor__first_name", "actor__last_name", - "actor__username", + f"actor__{get_user_model().USERNAME_FIELD}", ] list_filter = ["action", ResourceTypeFilter] readonly_fields = ["created", "resource_url", "action", "user_url", "msg"] From 993cd847fb246ca30d66f1a77c6542bb5a2cafe8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 20:57:48 +0200 Subject: [PATCH 026/126] [pre-commit.ci] pre-commit autoupdate (#435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.38.0 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.38.0...v2.38.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd35a223..bcb0f5cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.38.0 + rev: v2.38.2 hooks: - id: pyupgrade args: [--py37-plus] From 487b8ab5f43a3dd2c876518f9fa175997667e42d Mon Sep 17 00:00:00 2001 From: Youngkwang Yang Date: Thu, 6 Oct 2022 01:39:48 +0900 Subject: [PATCH 027/126] Remove unnecessary code in `created` method. (#438) --- auditlog/mixins.py | 18 +++++++----------- auditlog_tests/tests.py | 3 ++- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 1936763a..efa1ab52 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -2,6 +2,7 @@ from django import urls as urlresolvers from django.conf import settings +from django.contrib import admin from django.core.exceptions import FieldDoesNotExist from django.forms.utils import pretty_name from django.urls.exceptions import NoReverseMatch @@ -16,11 +17,11 @@ class LogEntryAdminMixin: + @admin.display(description="Created") def created(self, obj): - return localtime(obj.timestamp).strftime("%Y-%m-%d %H:%M:%S") - - created.short_description = "Created" + return localtime(obj.timestamp) + @admin.display(description="User") def user_url(self, obj): if obj.actor: app_label, model = settings.AUTH_USER_MODEL.split(".") @@ -33,8 +34,7 @@ def user_url(self, obj): return "system" - user_url.short_description = "User" - + @admin.display(description="Resource") def resource_url(self, obj): app_label, model = obj.content_type.app_label, obj.content_type.model viewname = f"admin:{app_label}_{model}_change" @@ -48,8 +48,7 @@ def resource_url(self, obj): '{} - {}', link, obj.content_type, obj.object_repr ) - resource_url.short_description = "Resource" - + @admin.display(description="Changes") def msg_short(self, obj): if obj.action == LogEntry.Action.DELETE: return "" # delete @@ -61,8 +60,7 @@ def msg_short(self, obj): fields = fields[:i] + " .." return "%d change%s: %s" % (len(changes), s, fields) - msg_short.short_description = "Changes" - + @admin.display(description="Changes") def msg(self, obj): changes = json.loads(obj.changes) @@ -114,8 +112,6 @@ def msg(self, obj): return mark_safe("".join(msg)) - msg.short_description = "Changes" - def _format_header(self, *labels): return format_html( "".join(["", "{}" * len(labels), ""]), *labels diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 0adb7105..92059023 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1294,7 +1294,8 @@ def test_created_timezone(self): ("Asia/Kathmandu", "2022-08-01 17:45:00"), ]: with self.settings(TIME_ZONE=tz): - self.assertEqual(self.admin.created(log_entry), timestamp) + created = self.admin.created(log_entry) + self.assertEqual(created.strftime("%Y-%m-%d %H:%M:%S"), timestamp) class DiffMsgTest(TestCase): From c04f5354ef3b5dd23cc2d6c6eff5977632d98065 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 22:08:10 +0200 Subject: [PATCH 028/126] [pre-commit.ci] pre-commit autoupdate (#440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0) - [github.com/asottile/pyupgrade: v2.38.2 → v3.0.0](https://github.com/asottile/pyupgrade/compare/v2.38.2...v3.0.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 bcb0f5cb..a51c6dd3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black language_version: python3.8 @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.38.2 + rev: v3.0.0 hooks: - id: pyupgrade args: [--py37-plus] From 90ce363b78bbc34530604b362832e0310c6657ff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 22:18:03 +0200 Subject: [PATCH 029/126] [pre-commit.ci] pre-commit autoupdate (#442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.0.0 → v3.1.0](https://github.com/asottile/pyupgrade/compare/v3.0.0...v3.1.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a51c6dd3..b7c05f2c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.0.0 + rev: v3.1.0 hooks: - id: pyupgrade args: [--py37-plus] From aa6d977f8b8b070690f9a168f2eddaa93a65df70 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Tue, 1 Nov 2022 09:00:42 +0100 Subject: [PATCH 030/126] Update pyupgrade and django-upgrade pre-commit hooks --- .pre-commit-config.yaml | 6 +++--- auditlog/admin.py | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7c05f2c..e847a714 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,12 +18,12 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.2.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.10.0 + rev: 1.11.0 hooks: - id: django-upgrade - args: [--target-version, "3.2"] \ No newline at end of file + args: [--target-version, "3.2"] diff --git a/auditlog/admin.py b/auditlog/admin.py index ee868e2a..83fe9bb5 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -6,6 +6,7 @@ from auditlog.models import LogEntry +@admin.register(LogEntry) class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): list_select_related = ["content_type", "actor"] list_display = ["created", "resource_url", "action", "msg_short", "user_url"] @@ -27,6 +28,3 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): def has_add_permission(self, request): # As audit admin doesn't allow log creation from admin return False - - -admin.site.register(LogEntry, LogEntryAdmin) From 8fe776ae45964d20bf113d5bff9287398f89480a Mon Sep 17 00:00:00 2001 From: Robin Harms Oredsson Date: Fri, 4 Nov 2022 09:12:06 +0100 Subject: [PATCH 031/126] Option to disable logging on raw save and via context manager (#446) * Disable on raw save prototype * Contextmanager to disable instead of just raw - so we can catch m2m relations too Co-authored-by: Hasan Ramezani --- CHANGELOG.md | 2 + auditlog/conf.py | 5 ++ auditlog/context.py | 12 ++++ auditlog/receivers.py | 25 ++++++++ auditlog/registry.py | 3 +- auditlog_tests/fixtures/m2m_test_fixture.json | 15 +++++ auditlog_tests/tests.py | 60 ++++++++++++++++++- docs/source/usage.rst | 39 +++++++++++- 8 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 auditlog_tests/fixtures/m2m_test_fixture.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6a8722..d87b91ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - 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` + to disable it during raw-save operations like loaddata. [#446](https://github.com/jazzband/django-auditlog/pull/446) #### Fixes diff --git a/auditlog/conf.py b/auditlog/conf.py index fdb685c3..4df8d879 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -15,3 +15,8 @@ settings.AUDITLOG_INCLUDE_TRACKING_MODELS = getattr( settings, "AUDITLOG_INCLUDE_TRACKING_MODELS", () ) + +# Disable on raw save to avoid logging imports and similar +settings.AUDITLOG_DISABLE_ON_RAW_SAVE = getattr( + settings, "AUDITLOG_DISABLE_ON_RAW_SAVE", False +) diff --git a/auditlog/context.py b/auditlog/context.py index 6e9513db..de2c0cad 100644 --- a/auditlog/context.py +++ b/auditlog/context.py @@ -64,3 +64,15 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs): instance.actor = user instance.remote_addr = auditlog["remote_addr"] + + +@contextlib.contextmanager +def disable_auditlog(): + threadlocal.auditlog_disabled = True + try: + yield + finally: + try: + del threadlocal.auditlog_disabled + except AttributeError: + pass diff --git a/auditlog/receivers.py b/auditlog/receivers.py index b6e867d5..81178703 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -1,9 +1,31 @@ import json +from functools import wraps +from django.conf import settings + +from auditlog.context import threadlocal from auditlog.diff import model_instance_diff from auditlog.models import LogEntry +def check_disable(signal_handler): + """ + Decorator that passes along disabled in kwargs if any of the following is true: + - 'auditlog_disabled' from threadlocal is true + - raw = True and AUDITLOG_DISABLE_ON_RAW_SAVE is True + """ + + @wraps(signal_handler) + def wrapper(*args, **kwargs): + if not getattr(threadlocal, "auditlog_disabled", False) and not ( + kwargs.get("raw") and settings.AUDITLOG_DISABLE_ON_RAW_SAVE + ): + signal_handler(*args, **kwargs) + + return wrapper + + +@check_disable def log_create(sender, instance, created, **kwargs): """ Signal receiver that creates a log entry when a model instance is first saved to the database. @@ -20,6 +42,7 @@ def log_create(sender, instance, created, **kwargs): ) +@check_disable def log_update(sender, instance, **kwargs): """ Signal receiver that creates a log entry when a model instance is changed and saved to the database. @@ -45,6 +68,7 @@ def log_update(sender, instance, **kwargs): ) +@check_disable def log_delete(sender, instance, **kwargs): """ Signal receiver that creates a log entry when a model instance is deleted from the database. @@ -64,6 +88,7 @@ def log_delete(sender, instance, **kwargs): def make_log_m2m_changes(field_name): """Return a handler for m2m_changed with field_name enclosed.""" + @check_disable 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"]: diff --git a/auditlog/registry.py b/auditlog/registry.py index d2ab0b0f..3bf36531 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -266,7 +266,8 @@ def register_from_settings(self): """ if not isinstance(settings.AUDITLOG_INCLUDE_ALL_MODELS, bool): raise TypeError("Setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be a boolean") - + if not isinstance(settings.AUDITLOG_DISABLE_ON_RAW_SAVE, bool): + raise TypeError("Setting 'AUDITLOG_DISABLE_ON_RAW_SAVE' must be a boolean") if not isinstance(settings.AUDITLOG_EXCLUDE_TRACKING_MODELS, (list, tuple)): raise TypeError( "Setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS' must be a list or tuple" diff --git a/auditlog_tests/fixtures/m2m_test_fixture.json b/auditlog_tests/fixtures/m2m_test_fixture.json new file mode 100644 index 00000000..c5c5d9e3 --- /dev/null +++ b/auditlog_tests/fixtures/m2m_test_fixture.json @@ -0,0 +1,15 @@ +[ + { + "model": "auditlog_tests.manyrelatedmodel", + "pk": 1, + "fields": { + "recursive": [1], + "related": [1] + } + }, + { + "model": "auditlog_tests.manyrelatedothermodel", + "pk": 1, + "fields": {} + } +] \ No newline at end of file diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 92059023..b43faaa4 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -12,12 +12,13 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser, User 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.utils import dateformat, formats, timezone from auditlog.admin import LogEntryAdmin -from auditlog.context import set_actor +from auditlog.context import disable_auditlog, set_actor from auditlog.diff import model_instance_diff from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry @@ -1092,6 +1093,12 @@ def test_register_from_settings_invalid_settings(self): ): self.test_auditlog.register_from_settings() + with override_settings(AUDITLOG_DISABLE_ON_RAW_SAVE="bad value"): + with self.assertRaisesMessage( + TypeError, "Setting 'AUDITLOG_DISABLE_ON_RAW_SAVE' must be a boolean" + ): + self.test_auditlog.register_from_settings() + @override_settings( AUDITLOG_INCLUDE_ALL_MODELS=True, AUDITLOG_EXCLUDE_TRACKING_MODELS=("auditlog_tests.SimpleExcludeModel",), @@ -1797,3 +1804,54 @@ def test_serialize_related_with_kwargs(self): "value": 11, }, ) + + +@override_settings(AUDITLOG_DISABLE_ON_RAW_SAVE=True) +class DisableTest(TestCase): + """ + All the other tests check logging, so this only needs to test disabled logging. + """ + + def test_create(self): + # Mimic the way imports create objects + inst = SimpleModel( + text="I am a bit more difficult.", boolean=False, datetime=timezone.now() + ) + SimpleModel.save_base(inst, raw=True) + self.assertEqual(0, LogEntry.objects.get_for_object(inst).count()) + + def test_create_with_context_manager(self): + with disable_auditlog(): + inst = SimpleModel.objects.create(text="I am a bit more difficult.") + self.assertEqual(0, LogEntry.objects.get_for_object(inst).count()) + + def test_update(self): + inst = SimpleModel( + text="I am a bit more difficult.", boolean=False, datetime=timezone.now() + ) + SimpleModel.save_base(inst, raw=True) + inst.text = "I feel refreshed" + inst.save_base(raw=True) + self.assertEqual(0, LogEntry.objects.get_for_object(inst).count()) + + def test_update_with_context_manager(self): + inst = SimpleModel( + text="I am a bit more difficult.", boolean=False, datetime=timezone.now() + ) + SimpleModel.save_base(inst, raw=True) + with disable_auditlog(): + inst.text = "I feel refreshed" + inst.save() + self.assertEqual(0, LogEntry.objects.get_for_object(inst).count()) + + def test_m2m(self): + """ + Create m2m from fixture and check that nothing was logged. + This only works with context manager + """ + with disable_auditlog(): + management.call_command("loaddata", "m2m_test_fixture.json", verbosity=0) + recursive = ManyRelatedModel.objects.get(pk=1) + self.assertEqual(0, LogEntry.objects.get_for_object(recursive).count()) + related = ManyRelatedOtherModel.objects.get(pk=1) + self.assertEqual(0, LogEntry.objects.get_for_object(related).count()) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 00abffd6..0675283f 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -201,6 +201,18 @@ It must be a list or tuple. Each item in this setting can be a: .. versionadded:: 2.1.0 +**AUDITLOG_DISABLE_ON_RAW_SAVE** + +Disables logging during raw save. (I.e. for instance using loaddata) + +.. note:: + + M2M operations will still be logged, since they're never considered `raw`. To disable them + you must remove their setting or use the `disable_auditlog` context manager. + +.. versionadded:: 2.2.0 + + Actors ------ @@ -228,10 +240,11 @@ It is recommended to keep all middleware that alters the request loaded before A user as actor. To only have some object changes to be logged with the current request's user as actor manual logging is required. -Context manager -*************** +Context managers +---------------- -.. versionadded:: 2.1.0 +Set actor +********* To enable the automatic logging of the actors outside of request context (e.g. in a Celery task), you can use a context manager:: @@ -244,6 +257,26 @@ manager:: # if your code here leads to creation of LogEntry instances, these will have the actor set ... + +.. versionadded:: 2.1.0 + + +Disable auditlog +**************** + +Disable auditlog temporary, for instance if you need to install a large fixture on a live system or cleanup +corrupt data:: + + from auditlog.context import disable_auditlog + + with disable_auditlog(): + # Do things silently here + ... + + +.. versionadded:: 2.2.0 + + Object history -------------- From f71699a9d0bb8a7d2a503aea950da2f1e7948187 Mon Sep 17 00:00:00 2001 From: Simon Kern Date: Mon, 7 Nov 2022 08:51:00 +0100 Subject: [PATCH 032/126] Added ACCESS action and enabled logging of object accesses (#436) --- CHANGELOG.md | 2 +- .../0012_add_logentry_action_access.py | 22 +++++++++++++++ auditlog/mixins.py | 10 ++++++- auditlog/models.py | 5 +++- auditlog/receivers.py | 15 ++++++++++ auditlog/registry.py | 6 +++- auditlog/signals.py | 3 ++ .../templates/simplemodel_detail.html | 0 auditlog_tests/tests.py | 28 +++++++++++++++++++ auditlog_tests/urls.py | 7 +++++ auditlog_tests/views.py | 9 ++++++ docs/source/usage.rst | 19 +++++++++++++ 12 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 auditlog/migrations/0012_add_logentry_action_access.py create mode 100644 auditlog/signals.py create mode 100644 auditlog_tests/templates/simplemodel_detail.html create mode 100644 auditlog_tests/views.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d87b91ef..f31dc4ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/auditlog/migrations/0012_add_logentry_action_access.py b/auditlog/migrations/0012_add_logentry_action_access.py new file mode 100644 index 00000000..76279416 --- /dev/null +++ b/auditlog/migrations/0012_add_logentry_action_access.py @@ -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", + ), + ), + ] diff --git a/auditlog/mixins.py b/auditlog/mixins.py index efa1ab52..56364803 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -12,6 +12,7 @@ from auditlog.models import LogEntry from auditlog.registry import auditlog +from auditlog.signals import accessed MAX = 75 @@ -50,7 +51,7 @@ def resource_url(self, obj): @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 @@ def field_verbose_name(self, obj, field_name: str): 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) diff --git a/auditlog/models.py b/auditlog/models.py index 63c154f4..aefc56a2 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -308,17 +308,20 @@ class Action: 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( diff --git a/auditlog/receivers.py b/auditlog/receivers.py index 81178703..2a2c475f 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -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.""" diff --git a/auditlog/registry.py b/auditlog/registry.py index 3bf36531..0d764555 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -24,6 +24,7 @@ ) from auditlog.conf import settings +from auditlog.signals import accessed DispatchUID = Tuple[int, int, int] @@ -44,10 +45,11 @@ def __init__( 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 @@ def __init__( 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: diff --git a/auditlog/signals.py b/auditlog/signals.py new file mode 100644 index 00000000..67e518c6 --- /dev/null +++ b/auditlog/signals.py @@ -0,0 +1,3 @@ +import django.dispatch + +accessed = django.dispatch.Signal() diff --git a/auditlog_tests/templates/simplemodel_detail.html b/auditlog_tests/templates/simplemodel_detail.html new file mode 100644 index 00000000..e69de29b diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index b43faaa4..3f60f056 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -15,6 +15,7 @@ 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 @@ def test_serialize_related_with_kwargs(self): ) +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): """ diff --git a/auditlog_tests/urls.py b/auditlog_tests/urls.py index 083932c6..712071d2 100644 --- a/auditlog_tests/urls.py +++ b/auditlog_tests/urls.py @@ -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//", + SimpleModelDetailview.as_view(), + name="simplemodel-detail", + ), ] diff --git a/auditlog_tests/views.py b/auditlog_tests/views.py new file mode 100644 index 00000000..436ecbfd --- /dev/null +++ b/auditlog_tests/views.py @@ -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" diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 0675283f..9e816411 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -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. From 36eaaaa2a9bf5c1f113b5479edf972439b1a6d39 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 7 Nov 2022 12:06:17 +0330 Subject: [PATCH 033/126] Confirm Python 3.11 support (#447) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 1 + setup.py | 1 + tox.ini | 7 +++++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1eae44aa..d9a12fe2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] services: postgres: diff --git a/CHANGELOG.md b/CHANGELOG.md index f31dc4ce..0418bd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 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` to disable it during raw-save operations like loaddata. [#446](https://github.com/jazzband/django-auditlog/pull/446) +- Python: Confirm Python 3.11 support ([#447](https://github.com/jazzband/django-auditlog/pull/447)) #### Fixes diff --git a/setup.py b/setup.py index 9135b245..e5e45cab 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", diff --git a/tox.ini b/tox.ini index e7fc0984..34ce2b42 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] envlist = {py37,py38,py39,py310}-django32 - {py38,py39,py310}-django{40,41,main} + {py38,py39,py310}-django40 + {py38,py39,py310,py311}-django{41,main} py37-docs py38-lint @@ -20,7 +21,7 @@ deps = coverage codecov freezegun - psycopg2-binary==2.8.6 + psycopg2-binary passenv= TEST_DB_HOST TEST_DB_USER @@ -29,6 +30,7 @@ passenv= TEST_DB_PORT basepython = + py311: python3.11 py310: python3.10 py39: python3.9 py38: python3.8 @@ -50,3 +52,4 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 From 2b0bc9efa2db94af2c5e130cce4e662cf3b2ad0e Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 7 Nov 2022 16:56:51 +0330 Subject: [PATCH 034/126] Replace the `django.utils.timezone.utc` by `datetime.timezone.utc` (#448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alieh Rymašeŭski --- CHANGELOG.md | 1 + auditlog/diff.py | 12 +++++++++--- auditlog/models.py | 3 ++- auditlog_tests/tests.py | 28 +++++++++++++++++++--------- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0418bd19..ac9cef74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - feat: New context manager `disable_auditlog` to turn off logging and a new setting `AUDITLOG_DISABLE_ON_RAW_SAVE` to disable it during raw-save operations like loaddata. [#446](https://github.com/jazzband/django-auditlog/pull/446) - Python: Confirm Python 3.11 support ([#447](https://github.com/jazzband/django-auditlog/pull/447)) +- feat: Replace the `django.utils.timezone.utc` by `datetime.timezone.utc`. [#448](https://github.com/jazzband/django-auditlog/pull/448) #### Fixes diff --git a/auditlog/diff.py b/auditlog/diff.py index 74bd9a02..e657ee14 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -1,7 +1,9 @@ +from datetime import timezone + from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.models import NOT_PROVIDED, DateTimeField, JSONField, Model -from django.utils import timezone +from django.utils import timezone as django_timezone from django.utils.encoding import smart_str @@ -63,8 +65,12 @@ def get_field_value(obj, field): # DateTimeFields are timezone-aware, so we need to convert the field # to its naive form before we can accurately compare them for changes. value = field.to_python(getattr(obj, field.name, None)) - if value is not None and settings.USE_TZ and not timezone.is_naive(value): - value = timezone.make_naive(value, timezone=timezone.utc) + if ( + value is not None + and settings.USE_TZ + and not django_timezone.is_naive(value) + ): + value = django_timezone.make_naive(value, timezone=timezone.utc) elif isinstance(field, JSONField): value = field.to_python(getattr(obj, field.name, None)) else: diff --git a/auditlog/models.py b/auditlog/models.py index aefc56a2..e073c882 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -1,6 +1,7 @@ import ast import json from copy import deepcopy +from datetime import timezone from typing import Any, Dict, List from dateutil import parser @@ -12,7 +13,7 @@ from django.core.exceptions import FieldDoesNotExist from django.db import DEFAULT_DB_ALIAS, models from django.db.models import Q, QuerySet -from django.utils import formats, timezone +from django.utils import formats from django.utils.encoding import smart_str from django.utils.translation import gettext_lazy as _ diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 3f60f056..2679efc6 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -2,6 +2,7 @@ import itertools import json import warnings +from datetime import timezone from unittest import mock import freezegun @@ -16,7 +17,8 @@ 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 django.utils import dateformat, formats +from django.utils import timezone as django_timezone from auditlog.admin import LogEntryAdmin from auditlog.context import disable_auditlog, set_actor @@ -618,8 +620,8 @@ def test_model_with_additional_data(self): class DateTimeFieldModelTest(TestCase): """Tests if DateTimeField changes are recognised correctly""" - utc_plus_one = timezone.get_fixed_timezone(datetime.timedelta(hours=1)) - now = timezone.now() + utc_plus_one = django_timezone.get_fixed_timezone(datetime.timedelta(hours=1)) + now = django_timezone.now() def setUp(self): super().setUp() @@ -788,7 +790,7 @@ def test_changes_display_dict_datetime(self): " DATETIME_FORMAT" ), ) - timestamp = timezone.now() + timestamp = django_timezone.now() dtm.timestamp = timestamp dtm.save() localized_timestamp = timestamp.astimezone(gettz(settings.TIME_ZONE)) @@ -912,7 +914,9 @@ def test_update_naive_dt(self): dtm.save() # Change with naive field doesnt raise error - dtm.naive_dt = timezone.make_naive(timezone.now(), timezone=timezone.utc) + dtm.naive_dt = django_timezone.make_naive( + django_timezone.now(), timezone=timezone.utc + ) dtm.save() @@ -1588,7 +1592,7 @@ def test_when_field_doesnt_exist(self): class TestModelSerialization(TestCase): def setUp(self): super().setUp() - self.test_date = datetime.datetime(2022, 1, 1, 12, tzinfo=datetime.timezone.utc) + self.test_date = datetime.datetime(2022, 1, 1, 12, tzinfo=timezone.utc) self.test_date_string = datetime.datetime.strftime( self.test_date, "%Y-%m-%dT%XZ" ) @@ -1843,7 +1847,9 @@ class DisableTest(TestCase): def test_create(self): # Mimic the way imports create objects inst = SimpleModel( - text="I am a bit more difficult.", boolean=False, datetime=timezone.now() + text="I am a bit more difficult.", + boolean=False, + datetime=django_timezone.now(), ) SimpleModel.save_base(inst, raw=True) self.assertEqual(0, LogEntry.objects.get_for_object(inst).count()) @@ -1855,7 +1861,9 @@ def test_create_with_context_manager(self): def test_update(self): inst = SimpleModel( - text="I am a bit more difficult.", boolean=False, datetime=timezone.now() + text="I am a bit more difficult.", + boolean=False, + datetime=django_timezone.now(), ) SimpleModel.save_base(inst, raw=True) inst.text = "I feel refreshed" @@ -1864,7 +1872,9 @@ def test_update(self): def test_update_with_context_manager(self): inst = SimpleModel( - text="I am a bit more difficult.", boolean=False, datetime=timezone.now() + text="I am a bit more difficult.", + boolean=False, + datetime=django_timezone.now(), ) SimpleModel.save_base(inst, raw=True) with disable_auditlog(): From 227b0d9fb54438127d22e3d6bbb99e1e5293270e Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 7 Nov 2022 17:18:05 +0330 Subject: [PATCH 035/126] Prepare release 2.2.0 (#434) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac9cef74..174e6e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changes +## 2.2.0 (2022-10-07) + #### 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)) From a6ea91f1bc15d4bb14bc3c58478fee19ce4121c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 22:14:53 +0330 Subject: [PATCH 036/126] [pre-commit.ci] pre-commit autoupdate (#451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.2.0 → v3.2.2](https://github.com/asottile/pyupgrade/compare/v3.2.0...v3.2.2) - [github.com/adamchainz/django-upgrade: 1.11.0 → 1.12.0](https://github.com/adamchainz/django-upgrade/compare/1.11.0...1.12.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 e847a714..0dced47d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,12 +18,12 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.2.0 + rev: v3.2.2 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.11.0 + rev: 1.12.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] From 96275d5386ef2f20e5fd5763dfdeb88679c8f95b Mon Sep 17 00:00:00 2001 From: Youngkwang Yang Date: Tue, 15 Nov 2022 21:05:35 +0900 Subject: [PATCH 037/126] Add `serialize_data` setting (#452) --- docs/source/usage.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 9e816411..7ff66058 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -214,6 +214,9 @@ It must be a list or tuple. Each item in this setting can be a: }, "mask_fields": ["field5", "field6"], "m2m_fields": ["field7", "field8"], + "serialize_data": True, + "serialize_auditlog_fields_only": False, + "serialize_kwargs": {"fields": ["foo", "bar", "biz", "baz"]}, }, ".", ) From 1ba3bd9d07d172c64368b0ded68ed7fdf1df4f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Mon, 21 Nov 2022 15:26:23 +0000 Subject: [PATCH 038/126] Disallow changing or deleting log entries (#449) --- CHANGELOG.md | 3 +++ auditlog/admin.py | 7 ++++++- auditlog_tests/tests.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 174e6e98..2e764974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changes +#### Fixes +- fix: Make log entries read-only in the admin. ([#449](https://github.com/jazzband/django-auditlog/pull/449)) + ## 2.2.0 (2022-10-07) #### Improvements diff --git a/auditlog/admin.py b/auditlog/admin.py index 83fe9bb5..0ba53543 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -26,5 +26,10 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): ] def has_add_permission(self, request): - # As audit admin doesn't allow log creation from admin + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): return False diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 2679efc6..52e7a8b9 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1292,7 +1292,7 @@ def test_auditlog_admin(self): res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/", follow=True) self.assertEqual(res.status_code, 200) res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/delete/") - self.assertEqual(res.status_code, 200) + self.assertEqual(res.status_code, 403) res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/history/") self.assertEqual(res.status_code, 200) From e23b091c99ae0ce18d484b7f6e1a13281f19e8db Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 21 Nov 2022 19:20:34 +0330 Subject: [PATCH 039/126] Replace `pkg_resources` with `importlib` solution (#450) --- auditlog/__init__.py | 20 ++++++++++++++------ docs/source/conf.py | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/auditlog/__init__.py b/auditlog/__init__.py index 50e82385..b758d0d0 100644 --- a/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -1,7 +1,15 @@ -from pkg_resources import DistributionNotFound, get_distribution - try: - __version__ = get_distribution("django-auditlog").version -except DistributionNotFound: - # package is not installed - pass + from importlib.metadata import version # New in Python 3.8 +except ImportError: + from pkg_resources import ( # from setuptools, deprecated + DistributionNotFound, + get_distribution, + ) + + try: + __version__ = get_distribution("django-auditlog").version + except DistributionNotFound: + # package is not installed + pass +else: + __version__ = version("django-auditlog") diff --git a/docs/source/conf.py b/docs/source/conf.py index 710a457d..f6689fa8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,7 +10,7 @@ import sys from datetime import date -from pkg_resources import get_distribution +from auditlog import __version__ # -- Path setup -------------------------------------------------------------- @@ -33,7 +33,7 @@ author = "Jan-Jelle Kester and contributors" copyright = f"2013-{date.today().year}, {author}" -release = get_distribution("django-auditlog").version +release = __version__ # for example take major/minor version = ".".join(release.split(".")[:2]) From 4cdc756791cac61d9b7023bd94f1c87b47555598 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Tue, 22 Nov 2022 10:44:59 +0300 Subject: [PATCH 040/126] FIX: Parsing client IPv6 address when a proxy is involved (#457) --- CHANGELOG.md | 4 ++++ auditlog/middleware.py | 19 +++++++++++++------ auditlog_tests/tests.py | 5 +++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e764974..8a4880b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ # Changes +## Next Release + #### Fixes + - fix: Make log entries read-only in the admin. ([#449](https://github.com/jazzband/django-auditlog/pull/449)) +- fix: Handle IPv6 addresses in `X-Forwarded-For`. ([#457](https://github.com/jazzband/django-auditlog/pull/457)) ## 2.2.0 (2022-10-07) diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 19875c9c..00745bca 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -14,14 +14,21 @@ def __init__(self, get_response=None): @staticmethod def _get_remote_addr(request): - if request.headers.get("X-Forwarded-For"): - # In case of proxy, set 'original' address - remote_addr = request.headers.get("X-Forwarded-For").split(",")[0] - # Remove port number from remote_addr - return remote_addr.split(":")[0] - else: + # In case there is no proxy, return the original address + if not request.headers.get("X-Forwarded-For"): return request.META.get("REMOTE_ADDR") + # In case of proxy, set 'original' address + remote_addr: str = request.headers.get("X-Forwarded-For").split(",")[0] + + # Remove port number from remote_addr + if "." in remote_addr and ":" in remote_addr: # IPv4 with port (`x.x.x.x:x`) + remote_addr = remote_addr.split(":")[0] + elif "[" in remote_addr: # IPv6 with port (`[:::]:x`) + remote_addr = remote_addr[1:].split("]")[0] + + return remote_addr + def __call__(self, request): remote_addr = self._get_remote_addr(request) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 52e7a8b9..e88c706e 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -450,6 +450,11 @@ def test_get_remote_addr(self): ({}, "127.0.0.1"), ({"HTTP_X_FORWARDED_FOR": "127.0.0.2"}, "127.0.0.2"), ({"HTTP_X_FORWARDED_FOR": "127.0.0.3:1234"}, "127.0.0.3"), + ({"HTTP_X_FORWARDED_FOR": "2606:4700:4700::1111"}, "2606:4700:4700::1111"), + ( + {"HTTP_X_FORWARDED_FOR": "[2606:4700:4700::1001]:1234"}, + "2606:4700:4700::1001", + ), ] for headers, expected_remote_addr in tests: with self.subTest(headers=headers): From b4dda75fc734484134d45538c2b2fe76c49be606 Mon Sep 17 00:00:00 2001 From: Ihor Sychevskyi Date: Mon, 28 Nov 2022 10:05:00 +0200 Subject: [PATCH 041/126] update readme link (#460) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ffae6b1d..6f0a94c3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ django-auditlog [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) [![Build Status](https://github.com/jazzband/django-auditlog/workflows/Test/badge.svg)](https://github.com/jazzband/django-auditlog/actions) -[![Docs](https://readthedocs.org/projects/django-auditlog/badge/?version=latest)](http://django-auditlog.readthedocs.org/en/latest/?badge=latest) +[![Docs](https://readthedocs.org/projects/django-auditlog/badge/?version=latest)](https://django-auditlog.readthedocs.org/en/latest/?badge=latest) [![codecov](https://codecov.io/gh/jazzband/django-auditlog/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/django-auditlog) [![Supported Python versions](https://img.shields.io/pypi/pyversions/django-auditlog.svg)](https://pypi.python.org/pypi/django-auditlog) [![Supported Django versions](https://img.shields.io/pypi/djversions/django-auditlog.svg)](https://pypi.python.org/pypi/django-auditlog) From 2a93c2086a704f91b900e461fcc343ec9c399464 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 28 Nov 2022 14:09:30 +0330 Subject: [PATCH 042/126] Prepare release 2.2.1 (#459) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4880b0..109bcdd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next Release +## 2.2.1 (2022-11-28) + #### Fixes - fix: Make log entries read-only in the admin. ([#449](https://github.com/jazzband/django-auditlog/pull/449)) From 1674acae1926ff7268b3f3600264ffb150967076 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 21:03:19 +0100 Subject: [PATCH 043/126] [pre-commit.ci] pre-commit autoupdate (#462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0dced47d..37adf46d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - "--target-version" - "py37" - repo: https://github.com/PyCQA/flake8 - rev: "5.0.4" + rev: "6.0.0" hooks: - id: flake8 args: ["--max-line-length", "110"] From bfaeeab74d8d8fd323e243bb0e3e536edc82fd16 Mon Sep 17 00:00:00 2001 From: Ihor Sychevskyi Date: Thu, 1 Dec 2022 09:22:03 +0200 Subject: [PATCH 044/126] update docs folder link (#465) --- docs/make.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.bat b/docs/make.bat index 447b8bdb..2ef50ff0 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -21,7 +21,7 @@ if errorlevel 9009 ( echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ + echo.https://sphinx-doc.org/ exit /b 1 ) From caf5daa2f8020d62bd5bec1b5f706cfb427ea07a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 08:49:35 +0100 Subject: [PATCH 045/126] [pre-commit.ci] pre-commit autoupdate (#466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.2.2 → v3.3.0](https://github.com/asottile/pyupgrade/compare/v3.2.2...v3.3.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37adf46d..a8b21457 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.3.0 hooks: - id: pyupgrade args: [--py37-plus] From cd1ba3d01b85cab6f62b3044ca7f028b7155473b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 09:18:31 +0100 Subject: [PATCH 046/126] [pre-commit.ci] pre-commit autoupdate (#473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0) - [github.com/asottile/pyupgrade: v3.3.0 → v3.3.1](https://github.com/asottile/pyupgrade/compare/v3.3.0...v3.3.1) 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 a8b21457..3cda7971 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black language_version: python3.8 @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.3.0 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py37-plus] From 27f57a53ffdc2053ccc22bfba9d6811f84a0b40a Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Tue, 13 Dec 2022 21:35:31 +0100 Subject: [PATCH 047/126] Fix LogEntry.changes_dict() to return {} when json.loads() returns None (#472) Co-authored-by: Hasan Ramezani --- CHANGELOG.md | 4 ++++ auditlog/models.py | 2 +- auditlog_tests/tests.py | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109bcdd4..5c9ee82d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Release +#### Fixes + +- fix: Make sure `LogEntry.changes_dict()` returns an empty dict instead of `None` when `json.loads()` returns `None`. ([#472](https://github.com/jazzband/django-auditlog/pull/472)) + ## 2.2.1 (2022-11-28) #### Fixes diff --git a/auditlog/models.py b/auditlog/models.py index e073c882..9a7340e8 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -387,7 +387,7 @@ def changes_dict(self): :return: The changes recorded in this log entry as a dictionary object. """ try: - return json.loads(self.changes) + return json.loads(self.changes) or {} except ValueError: return {} diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index e88c706e..9a004586 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1841,6 +1841,7 @@ def test_access_log(self): log_entry.action, LogEntry.Action.ACCESS, msg="Action is 'ACCESS'" ) self.assertEqual(log_entry.changes, "null") + self.assertEqual(log_entry.changes_dict, {}) @override_settings(AUDITLOG_DISABLE_ON_RAW_SAVE=True) From 703e3e4ba6ba7127cbd6a1bbf2597504089ba78c Mon Sep 17 00:00:00 2001 From: ZahraEbrahimi01 <93347447+ZahraEbrahimi01@users.noreply.github.com> Date: Wed, 14 Dec 2022 14:01:17 +0330 Subject: [PATCH 048/126] Complete translation with gettext_lazy (#474) --- auditlog/admin.py | 3 ++- auditlog/apps.py | 3 ++- auditlog/filters.py | 3 ++- auditlog/mixins.py | 11 ++++++----- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/auditlog/admin.py b/auditlog/admin.py index 0ba53543..a2e85115 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin from django.contrib.auth import get_user_model +from django.utils.translation import gettext_lazy as _ from auditlog.filters import ResourceTypeFilter from auditlog.mixins import LogEntryAdminMixin @@ -22,7 +23,7 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): readonly_fields = ["created", "resource_url", "action", "user_url", "msg"] fieldsets = [ (None, {"fields": ["created", "user_url", "resource_url"]}), - ("Changes", {"fields": ["action", "msg"]}), + (_("Changes"), {"fields": ["action", "msg"]}), ] def has_add_permission(self, request): diff --git a/auditlog/apps.py b/auditlog/apps.py index 0e9266e3..f6bf3fbd 100644 --- a/auditlog/apps.py +++ b/auditlog/apps.py @@ -1,9 +1,10 @@ from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ class AuditlogConfig(AppConfig): name = "auditlog" - verbose_name = "Audit log" + verbose_name = _("Audit log") default_auto_field = "django.db.models.AutoField" def ready(self): diff --git a/auditlog/filters.py b/auditlog/filters.py index 21591ac6..d2323c9e 100644 --- a/auditlog/filters.py +++ b/auditlog/filters.py @@ -1,8 +1,9 @@ from django.contrib.admin import SimpleListFilter +from django.utils.translation import gettext_lazy as _ class ResourceTypeFilter(SimpleListFilter): - title = "Resource Type" + title = _("Resource Type") parameter_name = "resource_type" def lookups(self, request, model_admin): diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 56364803..c5b430b9 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -9,6 +9,7 @@ from django.utils.html import format_html, format_html_join from django.utils.safestring import mark_safe from django.utils.timezone import localtime +from django.utils.translation import gettext_lazy as _ from auditlog.models import LogEntry from auditlog.registry import auditlog @@ -18,11 +19,11 @@ class LogEntryAdminMixin: - @admin.display(description="Created") + @admin.display(description=_("Created")) def created(self, obj): return localtime(obj.timestamp) - @admin.display(description="User") + @admin.display(description=_("User")) def user_url(self, obj): if obj.actor: app_label, model = settings.AUTH_USER_MODEL.split(".") @@ -35,7 +36,7 @@ def user_url(self, obj): return "system" - @admin.display(description="Resource") + @admin.display(description=_("Resource")) def resource_url(self, obj): app_label, model = obj.content_type.app_label, obj.content_type.model viewname = f"admin:{app_label}_{model}_change" @@ -49,7 +50,7 @@ def resource_url(self, obj): '{} - {}', link, obj.content_type, obj.object_repr ) - @admin.display(description="Changes") + @admin.display(description=_("Changes")) def msg_short(self, obj): if obj.action in [LogEntry.Action.DELETE, LogEntry.Action.ACCESS]: return "" # delete @@ -61,7 +62,7 @@ def msg_short(self, obj): fields = fields[:i] + " .." return "%d change%s: %s" % (len(changes), s, fields) - @admin.display(description="Changes") + @admin.display(description=_("Changes")) def msg(self, obj): changes = json.loads(obj.changes) From 8e496aadea827241296fe2c1eb9ca3c89050ef3d Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 15 Dec 2022 18:08:31 -0500 Subject: [PATCH 049/126] - using changes_dict to fix potential TypeError when changes are None in admin list view --- auditlog/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auditlog/mixins.py b/auditlog/mixins.py index c5b430b9..f593432c 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -54,7 +54,7 @@ def resource_url(self, obj): def msg_short(self, obj): if obj.action in [LogEntry.Action.DELETE, LogEntry.Action.ACCESS]: return "" # delete - changes = json.loads(obj.changes) + changes = obj.changes_dict s = "" if len(changes) == 1 else "s" fields = ", ".join(changes.keys()) if len(fields) > MAX: @@ -64,7 +64,7 @@ def msg_short(self, obj): @admin.display(description=_("Changes")) def msg(self, obj): - changes = json.loads(obj.changes) + changes = obj.changes_dict atom_changes = {} m2m_changes = {} From 971a4f42f8ce0ee8d376c6567199e1d223571c08 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 15 Dec 2022 18:30:58 -0500 Subject: [PATCH 050/126] - fixed unused import --- auditlog/mixins.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/auditlog/mixins.py b/auditlog/mixins.py index f593432c..84c18d01 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -1,5 +1,3 @@ -import json - from django import urls as urlresolvers from django.conf import settings from django.contrib import admin From a733cd0852d3ea292d34046234770797b07fb1ec Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Mon, 19 Dec 2022 02:43:29 -0500 Subject: [PATCH 051/126] feat: Make timestamp in LogEntry overwritable (#478) --- CHANGELOG.md | 1 + .../0013_alter_logentry_timestamp.py | 23 +++++++++++++++++++ auditlog/models.py | 5 +++- auditlog_tests/tests.py | 18 +++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 auditlog/migrations/0013_alter_logentry_timestamp.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9ee82d..49b7eb9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ #### Fixes - fix: Make sure `LogEntry.changes_dict()` returns an empty dict instead of `None` when `json.loads()` returns `None`. ([#472](https://github.com/jazzband/django-auditlog/pull/472)) +- feat: Make timestamp in LogEntry overwritable. ([#476](https://github.com/jazzband/django-auditlog/pull/476)) ## 2.2.1 (2022-11-28) diff --git a/auditlog/migrations/0013_alter_logentry_timestamp.py b/auditlog/migrations/0013_alter_logentry_timestamp.py new file mode 100644 index 00000000..a395916c --- /dev/null +++ b/auditlog/migrations/0013_alter_logentry_timestamp.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2022-12-15 21:24 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("auditlog", "0012_add_logentry_action_access"), + ] + + operations = [ + migrations.AlterField( + model_name="logentry", + name="timestamp", + field=models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + verbose_name="timestamp", + ), + ), + ] diff --git a/auditlog/models.py b/auditlog/models.py index 9a7340e8..79257732 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -14,6 +14,7 @@ from django.db import DEFAULT_DB_ALIAS, models from django.db.models import Q, QuerySet from django.utils import formats +from django.utils import timezone as django_timezone from django.utils.encoding import smart_str from django.utils.translation import gettext_lazy as _ @@ -355,7 +356,9 @@ class Action: blank=True, null=True, verbose_name=_("remote address") ) timestamp = models.DateTimeField( - db_index=True, auto_now_add=True, verbose_name=_("timestamp") + default=django_timezone.now, + db_index=True, + verbose_name=_("timestamp"), ) additional_data = models.JSONField( blank=True, null=True, verbose_name=_("additional data") diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 9a004586..1aa53692 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -200,6 +200,24 @@ def test_create_log_to_object_from_other_database(self): log_entry._state.db, "default", msg=msg ) # must be created in default database + def test_default_timestamp(self): + start = django_timezone.now() + self.test_recreate() + end = django_timezone.now() + history = self.obj.history.latest() + self.assertTrue(start <= history.timestamp <= end) + + def test_manual_timestamp(self): + timestamp = datetime.datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc) + LogEntry.objects.log_create( + instance=self.obj, + timestamp=timestamp, + changes="foo bar", + action=LogEntry.Action.UPDATE, + ) + history = self.obj.history.filter(timestamp=timestamp, changes="foo bar") + self.assertTrue(history.exists()) + class NoActorMixin: def check_create_log_entry(self, obj, log_entry): From 63c88829e02932ebd96b306ffc009edcba106dd1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 14:52:51 -0500 Subject: [PATCH 052/126] [pre-commit.ci] pre-commit autoupdate (#482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/isort: 5.10.1 → v5.11.3](https://github.com/PyCQA/isort/compare/5.10.1...v5.11.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3cda7971..8629de4e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: flake8 args: ["--max-line-length", "110"] - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: v5.11.3 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade From bc6d393390e5f746df18b07c5dfd82d6fac09c74 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Fri, 23 Dec 2022 09:09:32 -0500 Subject: [PATCH 053/126] Added support for Correlation ID Co-authored-by: Hasan Ramezani --- CHANGELOG.md | 4 ++ auditlog/admin.py | 19 +++++-- auditlog/cid.py | 66 +++++++++++++++++++++++ auditlog/conf.py | 7 +++ auditlog/filters.py | 16 ++++++ auditlog/middleware.py | 3 ++ auditlog/migrations/0014_logentry_cid.py | 24 +++++++++ auditlog/mixins.py | 19 +++++++ auditlog/models.py | 10 ++++ auditlog_tests/fixtures/custom_get_cid.py | 2 + auditlog_tests/tests.py | 53 +++++++++++++++++- docs/source/internals.rst | 6 +++ docs/source/usage.rst | 32 +++++++++-- 13 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 auditlog/cid.py create mode 100644 auditlog/migrations/0014_logentry_cid.py create mode 100644 auditlog_tests/fixtures/custom_get_cid.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b7eb9c..ff21e6a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Release +#### Improvements + +- feat: Added support for Correlation ID + #### Fixes - fix: Make sure `LogEntry.changes_dict()` returns an empty dict instead of `None` when `json.loads()` returns `None`. ([#472](https://github.com/jazzband/django-auditlog/pull/472)) diff --git a/auditlog/admin.py b/auditlog/admin.py index a2e85115..454a0f1e 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ -from auditlog.filters import ResourceTypeFilter +from auditlog.filters import CIDFilter, ResourceTypeFilter from auditlog.mixins import LogEntryAdminMixin from auditlog.models import LogEntry @@ -10,7 +10,14 @@ @admin.register(LogEntry) class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): list_select_related = ["content_type", "actor"] - list_display = ["created", "resource_url", "action", "msg_short", "user_url"] + list_display = [ + "created", + "resource_url", + "action", + "msg_short", + "user_url", + "cid_url", + ] search_fields = [ "timestamp", "object_repr", @@ -19,10 +26,10 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): "actor__last_name", f"actor__{get_user_model().USERNAME_FIELD}", ] - list_filter = ["action", ResourceTypeFilter] + list_filter = ["action", ResourceTypeFilter, CIDFilter] readonly_fields = ["created", "resource_url", "action", "user_url", "msg"] fieldsets = [ - (None, {"fields": ["created", "user_url", "resource_url"]}), + (None, {"fields": ["created", "user_url", "resource_url", "cid"]}), (_("Changes"), {"fields": ["action", "msg"]}), ] @@ -34,3 +41,7 @@ def has_change_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None): return False + + def get_queryset(self, request): + self.request = request + return super().get_queryset(request=request) diff --git a/auditlog/cid.py b/auditlog/cid.py new file mode 100644 index 00000000..e5308699 --- /dev/null +++ b/auditlog/cid.py @@ -0,0 +1,66 @@ +from contextvars import ContextVar +from typing import Optional + +from django.conf import settings +from django.http import HttpRequest +from django.utils.module_loading import import_string + +correlation_id = ContextVar("auditlog_correlation_id", default=None) + + +def set_cid(request: Optional[HttpRequest] = 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`. + + Note: we look for the header in `request.headers` and `request.META`. + + :param request: The request to get the cid from. + :return: None + """ + cid = None + header = settings.AUDITLOG_CID_HEADER + + if header and request: + if header in request.headers: + cid = request.headers.get(header) + elif header in request.META: + cid = request.META.get(header) + + # Ideally, this line should be nested inside the if statement. + # However, because the tests do not run requests in multiple threads, + # we have to always set the value of the cid, + # even if the request does not have the header present, + # in which case it will be set to None + correlation_id.set(cid) + + +def _get_cid() -> Optional[str]: + return correlation_id.get() + + +def get_cid() -> Optional[str]: + """ + Calls the cid getter function based on `settings.AUDITLOG_CID_GETTER` + + If the setting value is: + + * None: then it calls the default getter (which retrieves the value set in `set_cid`) + * callable: then it calls the function + * type(str): then it imports the function and then call it + + The result is then returned to the caller. + + If your custom getter does not depend on `set_header()`, + then we recommend setting `settings.AUDITLOG_CID_GETTER` to `None`. + + :return: The correlation ID + """ + method = settings.AUDITLOG_CID_GETTER + if not method: + return _get_cid() + + if callable(method): + return method() + + return import_string(method)() diff --git a/auditlog/conf.py b/auditlog/conf.py index 4df8d879..a56a165e 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -20,3 +20,10 @@ settings.AUDITLOG_DISABLE_ON_RAW_SAVE = getattr( settings, "AUDITLOG_DISABLE_ON_RAW_SAVE", False ) + +# CID + +settings.AUDITLOG_CID_HEADER = getattr( + settings, "AUDITLOG_CID_HEADER", "x-correlation-id" +) +settings.AUDITLOG_CID_GETTER = getattr(settings, "AUDITLOG_CID_GETTER", None) diff --git a/auditlog/filters.py b/auditlog/filters.py index d2323c9e..18a4c86b 100644 --- a/auditlog/filters.py +++ b/auditlog/filters.py @@ -15,3 +15,19 @@ def queryset(self, request, queryset): if self.value() is None: return queryset return queryset.filter(content_type_id=self.value()) + + +class CIDFilter(SimpleListFilter): + title = _("Correlation ID") + parameter_name = "cid" + + def lookups(self, request, model_admin): + return [] + + def has_output(self): + return True + + def queryset(self, request, queryset): + if self.value() is None: + return queryset + return queryset.filter(cid=self.value()) diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 00745bca..9d07ed79 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,5 +1,6 @@ import contextlib +from auditlog.cid import set_cid from auditlog.context import set_actor @@ -32,6 +33,8 @@ def _get_remote_addr(request): def __call__(self, request): remote_addr = self._get_remote_addr(request) + set_cid(request) + if hasattr(request, "user") and request.user.is_authenticated: context = set_actor(actor=request.user, remote_addr=remote_addr) else: diff --git a/auditlog/migrations/0014_logentry_cid.py b/auditlog/migrations/0014_logentry_cid.py new file mode 100644 index 00000000..57da8bcb --- /dev/null +++ b/auditlog/migrations/0014_logentry_cid.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.4 on 2022-12-18 13:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("auditlog", "0013_alter_logentry_timestamp"), + ] + + operations = [ + migrations.AddField( + model_name="logentry", + name="cid", + field=models.CharField( + blank=True, + db_index=True, + max_length=255, + null=True, + verbose_name="Correlation ID", + ), + ), + ] diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 84c18d01..e7b6155b 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -3,6 +3,7 @@ from django.contrib import admin from django.core.exceptions import FieldDoesNotExist from django.forms.utils import pretty_name +from django.http import HttpRequest from django.urls.exceptions import NoReverseMatch from django.utils.html import format_html, format_html_join from django.utils.safestring import mark_safe @@ -17,6 +18,9 @@ class LogEntryAdminMixin: + request: HttpRequest + CID_TITLE = _("Click to filter by records with this correlation id") + @admin.display(description=_("Created")) def created(self, obj): return localtime(obj.timestamp) @@ -112,6 +116,15 @@ def msg(self, obj): return mark_safe("".join(msg)) + @admin.display(description="Correlation ID") + def cid_url(self, obj): + cid = obj.cid + if cid: + url = self._add_query_parameter("cid", cid) + return format_html( + '{}', url, self.CID_TITLE, cid + ) + def _format_header(self, *labels): return format_html( "".join(["", "{}" * len(labels), ""]), *labels @@ -138,6 +151,12 @@ def field_verbose_name(self, obj, field_name: str): except FieldDoesNotExist: return pretty_name(field_name) + def _add_query_parameter(self, key: str, value: str): + full_path = self.request.get_full_path() + delimiter = "&" if "?" in full_path else "?" + + return f"{full_path}{delimiter}{key}={value}" + class LogAccessMixin: def render_to_response(self, context, **response_kwargs): diff --git a/auditlog/models.py b/auditlog/models.py index 79257732..d5b04ffb 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -37,6 +37,8 @@ def log_create(self, instance, **kwargs): :return: The new log entry or `None` if there were no changes. :rtype: LogEntry """ + from auditlog.cid import get_cid + changes = kwargs.get("changes", None) pk = self._get_pk_value(instance) @@ -76,6 +78,9 @@ def log_create(self, instance, **kwargs): content_type=kwargs.get("content_type"), object_pk=kwargs.get("object_pk", ""), ).delete() + + # set correlation id + kwargs.setdefault("cid", get_cid()) return self.create(**kwargs) return None @@ -96,6 +101,7 @@ def log_m2m_changes( :return: The new log entry or `None` if there were no changes. :rtype: LogEntry """ + from auditlog.cid import get_cid pk = self._get_pk_value(instance) if changed_queryset is not None: @@ -123,6 +129,7 @@ def log_m2m_changes( } } ) + kwargs.setdefault("cid", get_cid()) return self.create(**kwargs) return None @@ -352,6 +359,9 @@ class Action: related_name="+", verbose_name=_("actor"), ) + cid = models.CharField( + max_length=255, db_index=True, blank=True, verbose_name=_("Correlation ID") + ) remote_addr = models.GenericIPAddressField( blank=True, null=True, verbose_name=_("remote address") ) diff --git a/auditlog_tests/fixtures/custom_get_cid.py b/auditlog_tests/fixtures/custom_get_cid.py new file mode 100644 index 00000000..a8e70454 --- /dev/null +++ b/auditlog_tests/fixtures/custom_get_cid.py @@ -0,0 +1,2 @@ +def get_cid(): + return "my custom get_cid" diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 1aa53692..290fdc6a 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -21,11 +21,13 @@ from django.utils import timezone as django_timezone 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 LogEntry from auditlog.registry import AuditlogModelRegistry, AuditLogRegistrationError, auditlog +from auditlog_tests.fixtures.custom_get_cid import get_cid as custom_get_cid from auditlog_tests.models import ( AdditionalDataIncludedModel, AltPrimaryKeyModel, @@ -481,6 +483,38 @@ def test_get_remote_addr(self): self.middleware._get_remote_addr(request), expected_remote_addr ) + def test_cid(self): + header = str(settings.AUDITLOG_CID_HEADER).lstrip("HTTP_").replace("_", "-") + header_meta = "HTTP_" + header.upper().replace("-", "_") + cid = "random_CID" + + _settings = [ + # these tuples test reading the cid from the header defined in the settings + ({"AUDITLOG_CID_HEADER": header}, cid), # x-correlation-id + ({"AUDITLOG_CID_HEADER": header_meta}, cid), # HTTP_X_CORRELATION_ID + ({"AUDITLOG_CID_HEADER": None}, None), + # these two tuples test using a custom getter. + # Here, we don't necessarily care about the cid that was set in set_cid + ( + { + "AUDITLOG_CID_GETTER": "auditlog_tests.fixtures.custom_get_cid.get_cid" + }, + custom_get_cid(), + ), + ({"AUDITLOG_CID_GETTER": custom_get_cid}, custom_get_cid()), + ] + for setting, expected_result in _settings: + with self.subTest(): + with self.settings(**setting): + request = self.factory.get("/", **{header_meta: cid}) + self.middleware(request) + + obj = SimpleModel.objects.create(text="I am not difficult.") + history = obj.history.get(action=LogEntry.Action.CREATE) + + self.assertEqual(history.cid, expected_result) + self.assertEqual(get_cid(), expected_result) + class SimpleIncludeModelTest(TestCase): """Log only changes in include_fields""" @@ -593,7 +627,7 @@ def test_register_mapping_fields(self): ) -class SimpeMaskedFieldsModelTest(TestCase): +class SimpleMaskedFieldsModelTest(TestCase): """Log masked changes for fields in mask_fields""" def test_register_mask_fields(self): @@ -1214,7 +1248,7 @@ def test_changes_display_dict_many_to_one_relation(self): assert "related_models" in history.changes_display_dict -class CharfieldTextfieldModelTest(TestCase): +class CharFieldTextFieldModelTest(TestCase): def setUp(self): self.PLACEHOLDER_LONGCHAR = "s" * 255 self.PLACEHOLDER_LONGTEXTFIELD = "s" * 1000 @@ -1332,6 +1366,21 @@ def test_created_timezone(self): created = self.admin.created(log_entry) self.assertEqual(created.strftime("%Y-%m-%d %H:%M:%S"), timestamp) + def test_cid(self): + self.client.force_login(self.user) + expected_response = ( + '123' + ) + + log_entry = self.obj.history.latest() + log_entry.cid = "123" + log_entry.save() + + res = self.client.get("/admin/auditlog/logentry/") + self.assertEqual(res.status_code, 200) + self.assertIn(expected_response, res.rendered_content) + class DiffMsgTest(TestCase): def setUp(self): diff --git a/docs/source/internals.rst b/docs/source/internals.rst index 4964a06d..57163d2c 100644 --- a/docs/source/internals.rst +++ b/docs/source/internals.rst @@ -19,6 +19,12 @@ Middleware .. automodule:: auditlog.middleware :members: AuditlogMiddleware +Correlation ID +-------------- + +.. automodule:: auditlog.cid + :members: get_cid, set_cid + Signal receivers ---------------- diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 7ff66058..4961be2d 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -147,8 +147,8 @@ Objects are serialized using the Django core serializer. Keyword arguments may b .. code-block:: python auditlog.register( - MyModel, - serialize_data=True, + MyModel, + serialize_data=True, serialize_kwargs={"fields": ["foo", "bar", "biz", "baz"]} ) @@ -163,8 +163,18 @@ Note that all fields on the object will be serialized unless restricted with one serialize_auditlog_fields_only=True ) -Field masking is supported in object serialization. Any value belonging to a field whose name is found in the ``mask_fields`` list will be masked in the serialized object data. Masked values are obfuscated with asterisks in the same way as they are in the ``LogEntry.changes`` field. +Field masking is supported in object serialization. Any value belonging to a field whose name is found in the ``mask_fields`` list will be masked in the serialized object data. Masked values are obfuscated with asterisks in the same way as they are in the ``LogEntry.changes`` field. + +Correlation ID +-------------- + +You can store a correlation ID (cid) in the log entries by: + +1. Reading from a request header (specified by `AUDITLOG_CID_HEADER`) +2. Using a custom cid getter (specified by `AUDITLOG_CID_GETTER`) +Using the custom getter is helpful for integrating with a third-party cid package +such as `django-cid `_. Settings -------- @@ -214,7 +224,7 @@ It must be a list or tuple. Each item in this setting can be a: }, "mask_fields": ["field5", "field6"], "m2m_fields": ["field7", "field8"], - "serialize_data": True, + "serialize_data": True, "serialize_auditlog_fields_only": False, "serialize_kwargs": {"fields": ["foo", "bar", "biz", "baz"]}, }, @@ -234,6 +244,20 @@ Disables logging during raw save. (I.e. for instance using loaddata) .. versionadded:: 2.2.0 +**AUDITLOG_CID_HEADER** + +The request header containing the Correlation ID value to use in all log entries created as a result of the request. +The value can of in the format `HTTP_MY_HEADER` or `my-header`. + +.. versionadded:: 3.0.0 + +**AUDITLOG_CID_GETTER** + +The function to use to retrieve the Correlation ID. The value can be a callable or a string import path. + +If the value is `None`, the default getter will be used. + +.. versionadded:: 3.0.0 Actors ------ From 6996e1cfd462989552599b0d32c71be21f804593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Mon, 16 Jan 2023 15:04:51 +0000 Subject: [PATCH 054/126] Revert "Disallow changing or deleting log entries" (#496) This reverts commit de5638c607e781f2915e89ee3013c83846dfdfc7. --- CHANGELOG.md | 4 ++++ auditlog/admin.py | 7 +------ auditlog_tests/tests.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109bcdd4..1fc2f147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Release +#### Fixes + +- fix: revert [#449](https://github.com/jazzband/django-auditlog/pull/449) "Make log entries read-only in the admin" as it breaks deletion of any auditlogged model through the admin when `AuditlogHistoryField` is used. ([#496](https://github.com/jazzband/django-auditlog/pull/496)) + ## 2.2.1 (2022-11-28) #### Fixes diff --git a/auditlog/admin.py b/auditlog/admin.py index 0ba53543..83fe9bb5 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -26,10 +26,5 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): ] def has_add_permission(self, request): - return False - - def has_change_permission(self, request, obj=None): - return False - - def has_delete_permission(self, request, obj=None): + # As audit admin doesn't allow log creation from admin return False diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index e88c706e..edbc3134 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1297,7 +1297,7 @@ def test_auditlog_admin(self): res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/", follow=True) self.assertEqual(res.status_code, 200) res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/delete/") - self.assertEqual(res.status_code, 403) + self.assertEqual(res.status_code, 200) res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/history/") self.assertEqual(res.status_code, 200) From 2595a36c71ecacddb74d13f2764a50aaadc09a75 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Tue, 17 Jan 2023 11:37:11 +0330 Subject: [PATCH 055/126] Prepare release 2.2.2 (#497) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fc2f147..250193c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next Release +## 2.2.2 (2023-01-16) + #### Fixes - fix: revert [#449](https://github.com/jazzband/django-auditlog/pull/449) "Make log entries read-only in the admin" as it breaks deletion of any auditlogged model through the admin when `AuditlogHistoryField` is used. ([#496](https://github.com/jazzband/django-auditlog/pull/496)) From d73e94c4abf2344cdfe7da6490c8b56a53df4a92 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 28 Dec 2022 09:52:13 +0100 Subject: [PATCH 056/126] [pre-commit.ci] pre-commit autoupdate (#485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/isort: v5.11.3 → 5.11.4](https://github.com/PyCQA/isort/compare/v5.11.3...5.11.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8629de4e..609ff2d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: flake8 args: ["--max-line-length", "110"] - repo: https://github.com/PyCQA/isort - rev: v5.11.3 + rev: 5.11.4 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade From 4cde56922e0b16b5ca019ed36585e5fdfbb1d886 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 19 Jan 2023 11:50:45 +0330 Subject: [PATCH 057/126] Run django main tests on Python >= 3.10 (#499) --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a5ee3b72..7b10d3e7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,8 @@ envlist = {py37,py38,py39,py310}-django32 {py38,py39,py310}-django40 - {py38,py39,py310,py311}-django{41,main} + {py38,py39,py310,py311}-django41 + {py310,py311}-djangomain py37-docs py38-lint From 667a990a76f1dee076f28e417acdf6d10f155b81 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:17:32 +0100 Subject: [PATCH 058/126] [pre-commit.ci] pre-commit autoupdate (#504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/isort: 5.11.4 → 5.12.0](https://github.com/PyCQA/isort/compare/5.11.4...5.12.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 609ff2d6..06644c45 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: flake8 args: ["--max-line-length", "110"] - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade From 8e0870fa58291a86c1a4154416582dd71a156abf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 09:19:41 +0100 Subject: [PATCH 059/126] [pre-commit.ci] pre-commit autoupdate (#507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- auditlog/migrations/0001_initial.py | 1 - auditlog/migrations/0002_auto_support_long_primary_keys.py | 1 - auditlog/migrations/0003_logentry_remote_addr.py | 1 - auditlog/migrations/0004_logentry_detailed_object_repr.py | 1 - .../migrations/0005_logentry_additional_data_verbose_name.py | 1 - auditlog/migrations/0006_object_pk_index.py | 1 - auditlog/migrations/0007_object_pk_type.py | 1 - auditlog/migrations/0008_timestamp_index.py | 1 - auditlog/migrations/0009_timestamp_id_index.py | 1 - auditlog/migrations/0010_action_index.py | 1 - auditlog/migrations/0011_alter_logentry_additional_data.py | 1 - auditlog/migrations/0012_alter_logentry_timestamp.py | 1 - auditlog/receivers.py | 1 - 14 files changed, 1 insertion(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 06644c45..6eed31e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black language_version: python3.8 diff --git a/auditlog/migrations/0001_initial.py b/auditlog/migrations/0001_initial.py index 22b1ee4a..6821c2d5 100644 --- a/auditlog/migrations/0001_initial.py +++ b/auditlog/migrations/0001_initial.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("contenttypes", "0001_initial"), diff --git a/auditlog/migrations/0002_auto_support_long_primary_keys.py b/auditlog/migrations/0002_auto_support_long_primary_keys.py index e767b92f..5ab56893 100644 --- a/auditlog/migrations/0002_auto_support_long_primary_keys.py +++ b/auditlog/migrations/0002_auto_support_long_primary_keys.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0001_initial"), ] diff --git a/auditlog/migrations/0003_logentry_remote_addr.py b/auditlog/migrations/0003_logentry_remote_addr.py index c363e82b..b66bcd8a 100644 --- a/auditlog/migrations/0003_logentry_remote_addr.py +++ b/auditlog/migrations/0003_logentry_remote_addr.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0002_auto_support_long_primary_keys"), ] diff --git a/auditlog/migrations/0004_logentry_detailed_object_repr.py b/auditlog/migrations/0004_logentry_detailed_object_repr.py index b3762b93..d49b198e 100644 --- a/auditlog/migrations/0004_logentry_detailed_object_repr.py +++ b/auditlog/migrations/0004_logentry_detailed_object_repr.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0003_logentry_remote_addr"), ] diff --git a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py index f0ae841b..123080f7 100644 --- a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py +++ b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0004_logentry_detailed_object_repr"), ] diff --git a/auditlog/migrations/0006_object_pk_index.py b/auditlog/migrations/0006_object_pk_index.py index 729ebe20..a5c47b35 100644 --- a/auditlog/migrations/0006_object_pk_index.py +++ b/auditlog/migrations/0006_object_pk_index.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0005_logentry_additional_data_verbose_name"), ] diff --git a/auditlog/migrations/0007_object_pk_type.py b/auditlog/migrations/0007_object_pk_type.py index d6514e4e..97dc7812 100644 --- a/auditlog/migrations/0007_object_pk_type.py +++ b/auditlog/migrations/0007_object_pk_type.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0006_object_pk_index"), ] diff --git a/auditlog/migrations/0008_timestamp_index.py b/auditlog/migrations/0008_timestamp_index.py index 88c1128f..813a2abd 100644 --- a/auditlog/migrations/0008_timestamp_index.py +++ b/auditlog/migrations/0008_timestamp_index.py @@ -3,7 +3,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0007_object_pk_type"), ] diff --git a/auditlog/migrations/0009_timestamp_id_index.py b/auditlog/migrations/0009_timestamp_id_index.py index 1afce246..809d74d9 100644 --- a/auditlog/migrations/0009_timestamp_id_index.py +++ b/auditlog/migrations/0009_timestamp_id_index.py @@ -3,7 +3,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0008_timestamp_index"), ] diff --git a/auditlog/migrations/0010_action_index.py b/auditlog/migrations/0010_action_index.py index 048fc156..984ca10b 100644 --- a/auditlog/migrations/0010_action_index.py +++ b/auditlog/migrations/0010_action_index.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0009_timestamp_id_index"), ] diff --git a/auditlog/migrations/0011_alter_logentry_additional_data.py b/auditlog/migrations/0011_alter_logentry_additional_data.py index 07229059..af6623f3 100644 --- a/auditlog/migrations/0011_alter_logentry_additional_data.py +++ b/auditlog/migrations/0011_alter_logentry_additional_data.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0010_action_index"), ] diff --git a/auditlog/migrations/0012_alter_logentry_timestamp.py b/auditlog/migrations/0012_alter_logentry_timestamp.py index 8768ec13..cca7cd94 100644 --- a/auditlog/migrations/0012_alter_logentry_timestamp.py +++ b/auditlog/migrations/0012_alter_logentry_timestamp.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("auditlog", "0011_alter_logentry_additional_data"), ] diff --git a/auditlog/receivers.py b/auditlog/receivers.py index 2a2c475f..bc67f45a 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -92,7 +92,6 @@ def log_access(sender, instance, **kwargs): 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, From a65204a275ed8a0984ef807b90079866decdbdf3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Feb 2023 21:17:01 +0100 Subject: [PATCH 060/126] [pre-commit.ci] pre-commit autoupdate (#516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/adamchainz/django-upgrade: 1.12.0 → 1.13.0](https://github.com/adamchainz/django-upgrade/compare/1.12.0...1.13.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6eed31e9..cc808bba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.12.0 + rev: 1.13.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] From 356819676a054d3509c1cd9ed36f5b352da5f480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Sat, 1 Apr 2023 10:15:29 +0000 Subject: [PATCH 061/126] Fix GitHub actions deprecations (#520) * Upgrade versions of github actions See https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/ * Update syntax of set-output directive See https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ --- .github/workflows/release.yml | 8 ++++---- .github/workflows/test.yml | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55a6b6b3..3ca91dd7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,22 +11,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: 3.8 - name: Get pip cache dir id: pip-cache run: | - echo "::set-output name=dir::$(pip cache dir)" + echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: release-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d9a12fe2..3cc93d7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,20 +27,20 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Get pip cache dir id: pip-cache run: | - echo "::set-output name=dir::$(pip cache dir)" + echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: @@ -64,6 +64,6 @@ jobs: TEST_DB_PORT: ${{ job.services.postgres.ports[5432] }} - name: Upload coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: name: Python ${{ matrix.python-version }} From 7f6d646434e2b7468b0d550d4079893f7e9526e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 13:57:11 +0330 Subject: [PATCH 062/126] [pre-commit.ci] pre-commit autoupdate (#521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc808bba..d3b36f05 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black language_version: python3.8 From 87c4583c32546b4c75dcbe69065fdccf6f46ead8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 18:56:27 +0330 Subject: [PATCH 063/126] [pre-commit.ci] pre-commit autoupdate (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3b36f05..c6517169 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pyupgrade args: [--py37-plus] From 2365cee9cea965b0f54ca1db815f0c66209b6d53 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 May 2023 18:18:29 +0330 Subject: [PATCH 064/126] [pre-commit.ci] pre-commit autoupdate (#527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c6517169..6845361f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.4.0 hooks: - id: pyupgrade args: [--py37-plus] From 9093e611ce0938dc2fda3f91ce1b8dd30c7628cc Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 25 May 2023 11:15:01 +0330 Subject: [PATCH 065/126] Confirm Django 4.2 support (#529) --- setup.py | 1 + tox.ini | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9c6eb05..5397af5f 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", "License :: OSI Approved :: MIT License", ], ) diff --git a/tox.ini b/tox.ini index 7b10d3e7..43dc9d87 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = {py37,py38,py39,py310}-django32 {py38,py39,py310}-django40 - {py38,py39,py310,py311}-django41 + {py38,py39,py310,py311}-django{41,42} {py310,py311}-djangomain py37-docs py38-lint @@ -17,6 +17,7 @@ deps = django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 + django42: Django>=4.2,<4.3 djangomain: https://github.com/django/django/archive/main.tar.gz # Test requirements coverage From 816b837e6b10df7fef7f22d97112798f37178c71 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Jun 2023 15:24:50 +0300 Subject: [PATCH 066/126] [pre-commit.ci] pre-commit autoupdate (#534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.4.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.7.0) - [github.com/adamchainz/django-upgrade: 1.13.0 → 1.14.0](https://github.com/adamchainz/django-upgrade/compare/1.13.0...1.14.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 6845361f..696a0a52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,12 +18,12 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.7.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.13.0 + rev: 1.14.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] From 2630a0ef45257ee5db3b8e3e264c2491c968fd75 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 3 Jul 2023 12:17:35 +0200 Subject: [PATCH 067/126] Drop Django 4 support (#540) --- setup.py | 1 - tox.ini | 2 -- 2 files changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 5397af5f..d0225689 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,6 @@ "Programming Language :: Python :: 3.11", "Framework :: Django", "Framework :: Django :: 3.2", - "Framework :: Django :: 4.0", "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", "License :: OSI Approved :: MIT License", diff --git a/tox.ini b/tox.ini index 43dc9d87..a69b6681 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] envlist = {py37,py38,py39,py310}-django32 - {py38,py39,py310}-django40 {py38,py39,py310,py311}-django{41,42} {py310,py311}-djangomain py37-docs @@ -15,7 +14,6 @@ commands = coverage xml deps = django32: Django>=3.2,<3.3 - django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 django42: Django>=4.2,<4.3 djangomain: https://github.com/django/django/archive/main.tar.gz From 2c1a3e72583bdf6dea52f97b98bb4bc91fd08667 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 22:08:05 +0200 Subject: [PATCH 068/126] [pre-commit.ci] pre-commit autoupdate (#541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.7.0 → v3.8.0](https://github.com/asottile/pyupgrade/compare/v3.7.0...v3.8.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 696a0a52..62794935 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 + rev: v3.8.0 hooks: - id: pyupgrade args: [--py37-plus] From 682e8e270b12b07a673e34d7927ffd4abb4cf9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleh=20Ryma=C5=A1e=C5=ADski?= Date: Wed, 19 Jul 2023 10:15:27 +0000 Subject: [PATCH 069/126] Add null=True to LogEntry.cid field It's already set nullable in the migration. --- auditlog/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/auditlog/models.py b/auditlog/models.py index 1a09ca3b..30c3a6ad 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -360,7 +360,11 @@ class Action: verbose_name=_("actor"), ) cid = models.CharField( - max_length=255, db_index=True, blank=True, verbose_name=_("Correlation ID") + max_length=255, + db_index=True, + null=True, + blank=True, + verbose_name=_("Correlation ID"), ) remote_addr = models.GenericIPAddressField( blank=True, null=True, verbose_name=_("remote address") From 47188b46d790fa8870512b7497fb8152ee483c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleh=20Ryma=C5=A1e=C5=ADski?= Date: Thu, 31 Aug 2023 17:13:58 +0000 Subject: [PATCH 070/126] Stop deleting log entries in log_create --- CHANGELOG.md | 4 ++++ auditlog/models.py | 20 -------------------- auditlog/receivers.py | 2 +- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf02cb55..43ec504f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Release +#### Breaking Changes + +- feat: stop deleting old log entries when a model with the same pk is created (i.e. the pk value is reused) ([#559](https://github.com/jazzband/django-auditlog/pull/559)) + #### Improvements - feat: Added support for Correlation ID diff --git a/auditlog/models.py b/auditlog/models.py index 30c3a6ad..122cbc8d 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -59,26 +59,6 @@ def log_create(self, instance, **kwargs): if callable(get_additional_data): kwargs.setdefault("additional_data", get_additional_data()) - # Delete log entries with the same pk as a newly created model. - # This should only be necessary when an pk is used twice. - if kwargs.get("action", None) is LogEntry.Action.CREATE: - if ( - kwargs.get("object_id", None) is not None - and self.filter( - content_type=kwargs.get("content_type"), - object_id=kwargs.get("object_id"), - ).exists() - ): - self.filter( - content_type=kwargs.get("content_type"), - object_id=kwargs.get("object_id"), - ).delete() - else: - self.filter( - content_type=kwargs.get("content_type"), - object_pk=kwargs.get("object_pk", ""), - ).delete() - # set correlation id kwargs.setdefault("cid", get_cid()) return self.create(**kwargs) diff --git a/auditlog/receivers.py b/auditlog/receivers.py index bc67f45a..ccd336ac 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -49,7 +49,7 @@ def log_update(sender, instance, **kwargs): Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead. """ - if instance.pk is not None: + if not instance._state.adding: try: old = sender.objects.get(pk=instance.pk) except sender.DoesNotExist: From ab65364bb4a6c408f5115cee2b59780ca8655ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleh=20Ryma=C5=A1e=C5=ADski?= Date: Thu, 24 Aug 2023 15:25:56 +0000 Subject: [PATCH 071/126] Set history delete_related to False by default (#557) This is a breaking change. The default behavior is to delete the related objects, but it doesn't make sense to apply it to audit log entries, at least not by default. --- CHANGELOG.md | 1 + auditlog/models.py | 8 +++----- auditlog_tests/models.py | 30 +++++++++++++++--------------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ec504f..72559e25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ #### Breaking Changes - feat: stop deleting old log entries when a model with the same pk is created (i.e. the pk value is reused) ([#559](https://github.com/jazzband/django-auditlog/pull/559)) +- feat: Set `AuditlogHistoryField.delete_related` to `False` by default. This is different from the default configuration of Django's `GenericRelation`, but we should not erase the audit log of objects on deletion by default. ([#557](https://github.com/jazzband/django-auditlog/pull/557)) #### Improvements diff --git a/auditlog/models.py b/auditlog/models.py index 122cbc8d..2c0b1e44 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -503,14 +503,12 @@ class AuditlogHistoryField(GenericRelation): :param pk_indexable: Whether the primary key for this model is not an :py:class:`int` or :py:class:`long`. :type pk_indexable: bool - :param delete_related: By default, including a generic relation into a model will cause all related - objects to be cascade-deleted when the parent object is deleted. Passing False to this overrides this - behavior, retaining the full auditlog history for the object. Defaults to True, because that's - Django's default behavior. + :param delete_related: Delete referenced auditlog entries together with the tracked object. + Defaults to False to keep the integrity of the auditlog. :type delete_related: bool """ - def __init__(self, pk_indexable=True, delete_related=True, **kwargs): + def __init__(self, pk_indexable=True, delete_related=False, **kwargs): kwargs["to"] = LogEntry if pk_indexable: diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index 1a6c5a22..02f022cb 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -20,7 +20,7 @@ class SimpleModel(models.Model): integer = models.IntegerField(blank=True, null=True) datetime = models.DateTimeField(auto_now=True) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class AltPrimaryKeyModel(models.Model): @@ -35,7 +35,7 @@ class AltPrimaryKeyModel(models.Model): integer = models.IntegerField(blank=True, null=True) datetime = models.DateTimeField(auto_now=True) - history = AuditlogHistoryField(pk_indexable=False) + history = AuditlogHistoryField(delete_related=True, pk_indexable=False) class UUIDPrimaryKeyModel(models.Model): @@ -50,7 +50,7 @@ class UUIDPrimaryKeyModel(models.Model): integer = models.IntegerField(blank=True, null=True) datetime = models.DateTimeField(auto_now=True) - history = AuditlogHistoryField(pk_indexable=False) + history = AuditlogHistoryField(delete_related=True, pk_indexable=False) class ProxyModel(SimpleModel): @@ -80,7 +80,7 @@ class RelatedModel(RelatedModelParent): to="SimpleModel", on_delete=models.CASCADE, related_name="reverse_one_to_one" ) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class ManyRelatedModel(models.Model): @@ -91,7 +91,7 @@ class ManyRelatedModel(models.Model): recursive = models.ManyToManyField("self") related = models.ManyToManyField("ManyRelatedOtherModel", related_name="related") - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) def get_additional_data(self): related = self.related.first() @@ -103,7 +103,7 @@ class ManyRelatedOtherModel(models.Model): A model related to ManyRelatedModel as many-to-many. """ - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) @auditlog.register(include_fields=["label"]) @@ -115,7 +115,7 @@ class SimpleIncludeModel(models.Model): label = models.CharField(max_length=100) text = models.TextField(blank=True) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class SimpleExcludeModel(models.Model): @@ -126,7 +126,7 @@ class SimpleExcludeModel(models.Model): label = models.CharField(max_length=100) text = models.TextField(blank=True) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class SimpleMappingModel(models.Model): @@ -138,7 +138,7 @@ class SimpleMappingModel(models.Model): vtxt = models.CharField(verbose_name="Version", max_length=100) not_mapped = models.CharField(max_length=100) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) @auditlog.register(mask_fields=["address"]) @@ -150,7 +150,7 @@ class SimpleMaskedModel(models.Model): address = models.CharField(max_length=100) text = models.TextField() - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class AdditionalDataIncludedModel(models.Model): @@ -163,7 +163,7 @@ class AdditionalDataIncludedModel(models.Model): text = models.TextField(blank=True) related = models.ForeignKey(to=SimpleModel, on_delete=models.CASCADE) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) def get_additional_data(self): """ @@ -190,7 +190,7 @@ class DateTimeFieldModel(models.Model): time = models.TimeField() naive_dt = models.DateTimeField(null=True, blank=True) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class ChoicesFieldModel(models.Model): @@ -212,7 +212,7 @@ class ChoicesFieldModel(models.Model): status = models.CharField(max_length=1, choices=STATUS_CHOICES) multiplechoice = models.CharField(max_length=255, choices=STATUS_CHOICES) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class CharfieldTextfieldModel(models.Model): @@ -225,7 +225,7 @@ class CharfieldTextfieldModel(models.Model): longchar = models.CharField(max_length=255) longtextfield = models.TextField() - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class PostgresArrayFieldModel(models.Model): @@ -247,7 +247,7 @@ class PostgresArrayFieldModel(models.Model): models.CharField(max_length=1, choices=STATUS_CHOICES), size=3 ) - history = AuditlogHistoryField() + history = AuditlogHistoryField(delete_related=True) class NoDeleteHistoryModel(models.Model): From 408a105fe1769a36310bf9dd2612df29860803ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleh=20Ryma=C5=A1e=C5=ADski?= Date: Sun, 27 Aug 2023 09:12:39 +0000 Subject: [PATCH 072/126] Allow cascade deletion of auditlog entries (#556) * Allow cascade deletion of auditlog entries * Cache iteration over self.urls --- CHANGELOG.md | 1 + auditlog/admin.py | 13 ++++++++++++- auditlog_tests/tests.py | 19 ++++++++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72559e25..a9186a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - fix: Make sure `LogEntry.changes_dict()` returns an empty dict instead of `None` when `json.loads()` returns `None`. ([#472](https://github.com/jazzband/django-auditlog/pull/472)) - feat: Make timestamp in LogEntry overwritable. ([#476](https://github.com/jazzband/django-auditlog/pull/476)) +- fix: Make log entries read-only in the admin. ([#449](https://github.com/jazzband/django-auditlog/pull/449), [#556](https://github.com/jazzband/django-auditlog/pull/556)) (applied again after being reverted in 2.2.2) ## 2.2.2 (2023-01-16) diff --git a/auditlog/admin.py b/auditlog/admin.py index 52078593..6d3e3f2a 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -1,8 +1,9 @@ +from functools import cached_property + from django.conf import settings from django.contrib import admin from django.contrib.auth import get_user_model from django.core.paginator import Paginator -from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from auditlog.count import limit_query_time @@ -69,7 +70,17 @@ def has_add_permission(self, request): def has_change_permission(self, request, obj=None): return False + @cached_property + def _own_url_names(self): + return [pattern.name for pattern in self.urls if pattern.name] + def has_delete_permission(self, request, obj=None): + if ( + request.resolver_match + and request.resolver_match.url_name not in self._own_url_names + ): + # only allow cascade delete to satisfy delete_related flag + return super().has_delete_permission(request, obj) return False def get_queryset(self, request): diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index c9277416..313e8156 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -16,7 +16,7 @@ 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.urls import resolve, reverse from django.utils import dateformat, formats from django.utils import timezone as django_timezone @@ -1380,6 +1380,23 @@ def test_cid(self): self.assertEqual(res.status_code, 200) self.assertIn(expected_response, res.rendered_content) + def test_has_delete_permission(self): + log = self.obj.history.latest() + obj_pk = self.obj.pk + delete_log_request = RequestFactory().post( + f"/admin/auditlog/logentry/{log.pk}/delete/" + ) + delete_log_request.resolver_match = resolve(delete_log_request.path) + delete_log_request.user = self.user + delete_object_request = RequestFactory().post( + f"/admin/tests/simplemodel/{obj_pk}/delete/" + ) + delete_object_request.resolver_match = resolve(delete_object_request.path) + delete_object_request.user = self.user + + self.assertTrue(self.admin.has_delete_permission(delete_object_request, log)) + self.assertFalse(self.admin.has_delete_permission(delete_log_request, log)) + class DiffMsgTest(TestCase): def setUp(self): From ca3806f29662d27dec4b34fbeb344253314f9f93 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 13 Jul 2023 23:56:08 +0200 Subject: [PATCH 073/126] Drop support for Python 3.7 (#546) --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 4 ++-- CHANGELOG.md | 1 + auditlog/__init__.py | 16 ++-------------- docs/source/installation.rst | 6 +++--- pyproject.toml | 2 +- setup.py | 4 ++-- tox.ini | 8 +++----- 8 files changed, 15 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3cc93d7d..dd773a1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] services: postgres: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62794935..f12cc6ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: language_version: python3.8 args: - "--target-version" - - "py37" + - "py38" - repo: https://github.com/PyCQA/flake8 rev: "6.0.0" hooks: @@ -21,7 +21,7 @@ repos: rev: v3.8.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/adamchainz/django-upgrade rev: 1.14.0 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index a9186a28..9d168e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - feat: stop deleting old log entries when a model with the same pk is created (i.e. the pk value is reused) ([#559](https://github.com/jazzband/django-auditlog/pull/559)) - feat: Set `AuditlogHistoryField.delete_related` to `False` by default. This is different from the default configuration of Django's `GenericRelation`, but we should not erase the audit log of objects on deletion by default. ([#557](https://github.com/jazzband/django-auditlog/pull/557)) +- Python: Drop support for Python 3.7 ([#546](https://github.com/jazzband/django-auditlog/pull/546)) #### Improvements diff --git a/auditlog/__init__.py b/auditlog/__init__.py index b758d0d0..0fd293e3 100644 --- a/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -1,15 +1,3 @@ -try: - from importlib.metadata import version # New in Python 3.8 -except ImportError: - from pkg_resources import ( # from setuptools, deprecated - DistributionNotFound, - get_distribution, - ) +from importlib.metadata import version - try: - __version__ = get_distribution("django-auditlog").version - except DistributionNotFound: - # package is not installed - pass -else: - __version__ = version("django-auditlog") +__version__ = version("django-auditlog") diff --git a/docs/source/installation.rst b/docs/source/installation.rst index e6b0233a..0df17b78 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.7 or higher -- Django 3.2 or higher +- Python 3.8 or higher +- Django 3.2, 4.1 and 4.2 -Auditlog is currently tested with Python 3.7+ and Django 3.2 and 4.0. The latest test report can be found +Auditlog is currently tested with Python 3.8+ and Django 3.2, 4.1 and 4.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/pyproject.toml b/pyproject.toml index be27ad00..316468f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -target-version = ["py37"] +target-version = ["py38"] [tool.isort] profile = "black" diff --git a/setup.py b/setup.py index d0225689..de2471ba 100644 --- a/setup.py +++ b/setup.py @@ -28,15 +28,15 @@ description="Audit log app for Django", long_description=long_description, long_description_content_type="text/markdown", - python_requires=">=3.7", + python_requires=">=3.8", install_requires=[ + "Django>=3.2", "django-admin-rangefilter>=0.8.0", "python-dateutil>=2.7.0", ], zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/tox.ini b/tox.ini index a69b6681..98da3309 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] envlist = - {py37,py38,py39,py310}-django32 + {py38,py39,py310}-django32 {py38,py39,py310,py311}-django{41,42} {py310,py311}-djangomain - py37-docs + py38-docs py38-lint [testenv] @@ -35,9 +35,8 @@ basepython = py310: python3.10 py39: python3.9 py38: python3.8 - py37: python3.7 -[testenv:py37-docs] +[testenv:py38-docs] changedir = docs/source deps = -rdocs/requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html @@ -49,7 +48,6 @@ commands = [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 From 0807b8f2160093a8e5903292eca53faf1990f3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleh=20Ryma=C5=A1e=C5=ADski?= Date: Tue, 24 Oct 2023 11:55:26 +0000 Subject: [PATCH 074/126] Allow @auditlog.register with no parentheses --- auditlog/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auditlog/registry.py b/auditlog/registry.py index 1f8166be..38e22a48 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -141,7 +141,7 @@ def registrar(cls): return lambda cls: registrar(cls) else: # Otherwise, just register the model. - registrar(model) + return registrar(model) def contains(self, model: ModelBase) -> bool: """ From e3f547c8821eefd6159c177b3a4e559a27840142 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Tue, 27 Dec 2022 14:14:51 -0500 Subject: [PATCH 075/126] Adding Custom Pre- and Post- Log Hooks (#483) --- CHANGELOG.md | 3 +- auditlog/diff.py | 5 +- auditlog/receivers.py | 90 ++++++++++++++------- auditlog/signals.py | 50 ++++++++++++ auditlog_tests/tests.py | 164 ++++++++++++++++++++++++++++++++++++++ docs/source/internals.rst | 12 +++ 6 files changed, 293 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d168e73..a2158e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ #### Improvements -- feat: Added support for Correlation ID +- feat: Added support for Correlation ID. ([#481](https://github.com/jazzband/django-auditlog/pull/481)) +- feat: Added pre-log and post-log signals. ([#483](https://github.com/jazzband/django-auditlog/pull/483)) #### Fixes diff --git a/auditlog/diff.py b/auditlog/diff.py index e657ee14..0d25c861 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -1,4 +1,5 @@ from datetime import timezone +from typing import Optional from django.conf import settings from django.core.exceptions import ObjectDoesNotExist @@ -98,7 +99,9 @@ def mask_str(value: str) -> str: return "*" * mask_limit + value[mask_limit:] -def model_instance_diff(old, new, fields_to_check=None): +def model_instance_diff( + old: Optional[Model], new: Optional[Model], fields_to_check=None +): """ Calculates the differences between two model instances. One of the instances may be ``None`` (i.e., a newly created model or deleted model). This will cause all fields with a value to have diff --git a/auditlog/receivers.py b/auditlog/receivers.py index ccd336ac..a78da458 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -6,6 +6,7 @@ from auditlog.context import threadlocal from auditlog.diff import model_instance_diff from auditlog.models import LogEntry +from auditlog.signals import post_log, pre_log def check_disable(signal_handler): @@ -33,12 +34,12 @@ def log_create(sender, instance, created, **kwargs): Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead. """ if created: - changes = model_instance_diff(None, instance) - - LogEntry.objects.log_create( - instance, + _create_log_entry( action=LogEntry.Action.CREATE, - changes=json.dumps(changes), + instance=instance, + sender=sender, + diff_old=None, + diff_new=instance, ) @@ -50,22 +51,16 @@ def log_update(sender, instance, **kwargs): Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead. """ if not instance._state.adding: - try: - old = sender.objects.get(pk=instance.pk) - except sender.DoesNotExist: - pass - else: - new = instance - update_fields = kwargs.get("update_fields", None) - changes = model_instance_diff(old, new, fields_to_check=update_fields) - - # Log an entry only if there are changes - if changes: - LogEntry.objects.log_create( - instance, - action=LogEntry.Action.UPDATE, - changes=json.dumps(changes), - ) + update_fields = kwargs.get("update_fields", None) + old = sender.objects.filter(pk=instance.pk).first() + _create_log_entry( + action=LogEntry.Action.UPDATE, + instance=instance, + sender=sender, + diff_old=old, + diff_new=instance, + fields_to_check=update_fields, + ) @check_disable @@ -76,12 +71,12 @@ def log_delete(sender, instance, **kwargs): Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead. """ if instance.pk is not None: - changes = model_instance_diff(instance, None) - - LogEntry.objects.log_create( - instance, + _create_log_entry( action=LogEntry.Action.DELETE, - changes=json.dumps(changes), + instance=instance, + sender=sender, + diff_old=instance, + diff_new=None, ) @@ -92,11 +87,48 @@ def log_access(sender, instance, **kwargs): 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, + _create_log_entry( action=LogEntry.Action.ACCESS, - changes="null", + instance=instance, + sender=sender, + diff_old=None, + diff_new=None, + force_log=True, + ) + + +def _create_log_entry( + action, instance, sender, diff_old, diff_new, fields_to_check=None, force_log=False +): + pre_log_results = pre_log.send( + sender, + instance=instance, + action=action, + ) + error = None + try: + changes = model_instance_diff( + diff_old, diff_new, fields_to_check=fields_to_check + ) + + if force_log or changes: + LogEntry.objects.log_create( + instance, + action=action, + changes=json.dumps(changes), + ) + except BaseException as e: + error = e + finally: + post_log.send( + sender, + instance=instance, + action=action, + error=error, + pre_log_results=pre_log_results, ) + if error: + raise error def make_log_m2m_changes(field_name): diff --git a/auditlog/signals.py b/auditlog/signals.py index 67e518c6..aec291a6 100644 --- a/auditlog/signals.py +++ b/auditlog/signals.py @@ -1,3 +1,53 @@ import django.dispatch accessed = django.dispatch.Signal() + + +pre_log = django.dispatch.Signal() +""" +Whenever an audit log entry is written, this signal +is sent before writing the log. +Keyword arguments sent with this signal: + +:param class sender: + The model class that's being audited. + +:param Any instance: + The actual instance that's being audited. + +:param Action action: + The action on the model resulting in an + audit log entry. Type: :class:`auditlog.models.LogEntry.Action` + +The receivers' return values are sent to any :func:`post_log` +signal receivers. +""" + +post_log = django.dispatch.Signal() +""" +Whenever an audit log entry is written, this signal +is sent after writing the log. +Keyword arguments sent with this signal: + +:param class sender: + The model class that's being audited. + +:param Any instance: + The actual instance that's being audited. + +:param Action action: + The action on the model resulting in an + audit log entry. Type: :class:`auditlog.models.LogEntry.Action` + +:param Optional[Exception] error: + The error, if one occurred while saving the audit log entry. ``None``, + otherwise + +:param List[Tuple[method,Any]] pre_log_results: + List of tuple pairs ``[(pre_log_receiver, pre_log_response)]``, where + ``pre_log_receiver`` is the receiver method, and ``pre_log_response`` is the + corresponding response of that method. If there are no :const:`pre_log` receivers, + then the list will be empty. ``pre_log_receiver`` is guaranteed to be + non-null, but ``pre_log_response`` may be ``None``. This depends on the corresponding + ``pre_log_receiver``'s return value. +""" diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 313e8156..f51ef05e 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1,9 +1,11 @@ import datetime import itertools import json +import random import warnings from datetime import timezone from unittest import mock +from unittest.mock import patch import freezegun from dateutil.tz import gettz @@ -27,6 +29,7 @@ from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry from auditlog.registry import AuditlogModelRegistry, AuditLogRegistrationError, auditlog +from auditlog.signals import post_log, pre_log from auditlog_tests.fixtures.custom_get_cid import get_cid as custom_get_cid from auditlog_tests.models import ( AdditionalDataIncludedModel, @@ -1927,6 +1930,167 @@ def test_access_log(self): self.assertEqual(log_entry.changes_dict, {}) +class SignalTests(TestCase): + def setUp(self): + self.obj = SimpleModel.objects.create(text="I am not difficult.") + self.my_pre_log_data = { + "is_called": False, + "my_sender": None, + "my_instance": None, + "my_action": None, + } + self.my_post_log_data = { + "is_called": False, + "my_sender": None, + "my_instance": None, + "my_action": None, + "my_error": None, + } + + def assertSignals(self, action): + self.assertTrue( + self.my_pre_log_data["is_called"], "pre_log hook receiver not called" + ) + self.assertIs(self.my_pre_log_data["my_sender"], self.obj.__class__) + self.assertIs(self.my_pre_log_data["my_instance"], self.obj) + self.assertEqual(self.my_pre_log_data["my_action"], action) + + self.assertTrue( + self.my_post_log_data["is_called"], "post_log hook receiver not called" + ) + self.assertIs(self.my_post_log_data["my_sender"], self.obj.__class__) + self.assertIs(self.my_post_log_data["my_instance"], self.obj) + self.assertEqual(self.my_post_log_data["my_action"], action) + self.assertIsNone(self.my_post_log_data["my_error"]) + + def test_custom_signals(self): + my_ret_val = random.randint(0, 10000) + my_other_ret_val = random.randint(0, 10000) + + def pre_log_receiver(sender, instance, action, **_kwargs): + self.my_pre_log_data["is_called"] = True + self.my_pre_log_data["my_sender"] = sender + self.my_pre_log_data["my_instance"] = instance + self.my_pre_log_data["my_action"] = action + return my_ret_val + + def pre_log_receiver_extra(*_args, **_kwargs): + return my_other_ret_val + + def post_log_receiver( + sender, instance, action, error, pre_log_results, **_kwargs + ): + self.my_post_log_data["is_called"] = True + self.my_post_log_data["my_sender"] = sender + self.my_post_log_data["my_instance"] = instance + self.my_post_log_data["my_action"] = action + self.my_post_log_data["my_error"] = error + + self.assertEqual(len(pre_log_results), 2) + + found_first_result = False + found_second_result = False + for pre_log_fn, pre_log_result in pre_log_results: + if pre_log_fn is pre_log_receiver and pre_log_result == my_ret_val: + found_first_result = True + for pre_log_fn, pre_log_result in pre_log_results: + if ( + pre_log_fn is pre_log_receiver_extra + and pre_log_result == my_other_ret_val + ): + found_second_result = True + + self.assertTrue(found_first_result) + self.assertTrue(found_second_result) + + return my_ret_val + + pre_log.connect(pre_log_receiver) + pre_log.connect(pre_log_receiver_extra) + post_log.connect(post_log_receiver) + + self.obj = SimpleModel.objects.create(text="I am not difficult.") + + self.assertSignals(LogEntry.Action.CREATE) + + def test_custom_signals_update(self): + def pre_log_receiver(sender, instance, action, **_kwargs): + self.my_pre_log_data["is_called"] = True + self.my_pre_log_data["my_sender"] = sender + self.my_pre_log_data["my_instance"] = instance + self.my_pre_log_data["my_action"] = action + + def post_log_receiver(sender, instance, action, error, **_kwargs): + self.my_post_log_data["is_called"] = True + self.my_post_log_data["my_sender"] = sender + self.my_post_log_data["my_instance"] = instance + self.my_post_log_data["my_action"] = action + self.my_post_log_data["my_error"] = error + + pre_log.connect(pre_log_receiver) + post_log.connect(post_log_receiver) + + self.obj.text = "Changed Text" + self.obj.save() + + self.assertSignals(LogEntry.Action.UPDATE) + + def test_custom_signals_delete(self): + def pre_log_receiver(sender, instance, action, **_kwargs): + self.my_pre_log_data["is_called"] = True + self.my_pre_log_data["my_sender"] = sender + self.my_pre_log_data["my_instance"] = instance + self.my_pre_log_data["my_action"] = action + + def post_log_receiver(sender, instance, action, error, **_kwargs): + self.my_post_log_data["is_called"] = True + self.my_post_log_data["my_sender"] = sender + self.my_post_log_data["my_instance"] = instance + self.my_post_log_data["my_action"] = action + self.my_post_log_data["my_error"] = error + + pre_log.connect(pre_log_receiver) + post_log.connect(post_log_receiver) + + self.obj.delete() + + self.assertSignals(LogEntry.Action.DELETE) + + @patch("auditlog.receivers.LogEntry.objects") + def test_signals_errors(self, log_entry_objects_mock): + class CustomSignalError(BaseException): + pass + + def post_log_receiver(error, **_kwargs): + self.my_post_log_data["my_error"] = error + + post_log.connect(post_log_receiver) + + # create + error_create = CustomSignalError(LogEntry.Action.CREATE) + log_entry_objects_mock.log_create.side_effect = error_create + with self.assertRaises(CustomSignalError): + SimpleModel.objects.create(text="I am not difficult.") + self.assertEqual(self.my_post_log_data["my_error"], error_create) + + # update + error_update = CustomSignalError(LogEntry.Action.UPDATE) + log_entry_objects_mock.log_create.side_effect = error_update + with self.assertRaises(CustomSignalError): + obj = SimpleModel.objects.get(pk=self.obj.pk) + obj.text = "updating" + obj.save() + self.assertEqual(self.my_post_log_data["my_error"], error_update) + + # delete + error_delete = CustomSignalError(LogEntry.Action.DELETE) + log_entry_objects_mock.log_create.side_effect = error_delete + with self.assertRaises(CustomSignalError): + obj = SimpleModel.objects.get(pk=self.obj.pk) + obj.delete() + self.assertEqual(self.my_post_log_data["my_error"], error_delete) + + @override_settings(AUDITLOG_DISABLE_ON_RAW_SAVE=True) class DisableTest(TestCase): """ diff --git a/docs/source/internals.rst b/docs/source/internals.rst index 57163d2c..9c869c12 100644 --- a/docs/source/internals.rst +++ b/docs/source/internals.rst @@ -31,6 +31,18 @@ Signal receivers .. automodule:: auditlog.receivers :members: +Custom Signals +-------------- +Django Auditlog provides two custom signals that will hook in before +and after any Auditlog record is written from a ``create``, ``update``, +``delete``, or ``accessed`` action on an audited model. + +.. automodule:: auditlog.signals + :members: + :member-order: bysource + +.. versionadded:: 3.0.0 + Calculating changes ------------------- From e3f6c026e1f6c68aae6a553fa168a9f975acd175 Mon Sep 17 00:00:00 2001 From: August Raack <60975983+sum-rock@users.noreply.github.com> Date: Wed, 28 Dec 2022 02:50:35 -0600 Subject: [PATCH 076/126] Modify ``change`` field to be a json field. (#407) * Modify ``change`` field to be a json field. Storing the object changes as a json is preferred because it allows SQL queries to access the change values. This work moves the burden of handling json objects from an implementation of python's json library in this package and puts it instead onto the ORM. Ultimately, having the text field store the changes was leaving them less accessible to external systems and code that is written outside the scope of the django auditlog. This change was accomplished by updating the field type on the model and then removing the JSON dumps invocations on write and JSON loads invocations on read. Test were updated to assert equality of dictionaries rather than equality of JSON parsable text. Separately, it was asserted that postgres will make these changes to existing data. Therefore, existing postgres installations should update the type of existing field values without issue. * Add test coverage for messages exceeding char len The "Modify change field to be a json field" commit reduced test coverage on the mixins.py file by 0.03%. The reduction in coverage was the result of reducing the number of operations required to achieve the desired state. An additional test was added to increase previously uncovered code. The net effect is an increase in test case coverage. * Add line to changelog Better markdown formatting Co-authored-by: Hasan Ramezani * Update CHANGELOG text format More specific language in the improvement section regarding `LogEntry.change` Co-authored-by: Hasan Ramezani * Update migration to show Django version 4.0 Co-authored-by: Hasan Ramezani * Update CHANGELOG to show breaking change Running the migration to update the field type of `LogEntry.change` is a breaking change. Co-authored-by: Hasan Ramezani * Update serial order of migrations * Adjust manager method for compatibility The create log method on the LogEntry manager required an additional kwarg for a call to create an instance regardless of a change or not. This felt brittle anyway. The reason it had worked prior to these changes was that the `change` kwarg was sending a string "null" and not a None when there were no changes. Co-authored-by: Hasan Ramezani --- CHANGELOG.md | 4 ++ .../migrations/0017_alter_logentry_changes.py | 18 ++++++++ auditlog/models.py | 13 +++--- auditlog/receivers.py | 4 +- auditlog_tests/tests.py | 44 ++++++++++++++----- 5 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 auditlog/migrations/0017_alter_logentry_changes.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a2158e17..ca7bf756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes +#### Breaking Changes + +- feat: Change `LogEntry.change` field type to `JSONField` rather than `TextField`. This change include a migration that may take time to run depending on the number of records on your `LogEntry` table ([#407](https://github.com/jazzband/django-auditlog/pull/407)) + ## Next Release #### Breaking Changes diff --git a/auditlog/migrations/0017_alter_logentry_changes.py b/auditlog/migrations/0017_alter_logentry_changes.py new file mode 100644 index 00000000..b8e685fb --- /dev/null +++ b/auditlog/migrations/0017_alter_logentry_changes.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0 on 2022-08-04 15:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("auditlog", "0016_logentry_cid"), + ] + + operations = [ + migrations.AlterField( + model_name="logentry", + name="changes", + field=models.JSONField(null=True, verbose_name="change message"), + ), + ] diff --git a/auditlog/models.py b/auditlog/models.py index 2c0b1e44..5ccb960f 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -26,13 +26,15 @@ class LogEntryManager(models.Manager): Custom manager for the :py:class:`LogEntry` model. """ - def log_create(self, instance, **kwargs): + def log_create(self, instance, force_log: bool = False, **kwargs): """ Helper method to create a new log entry. This method automatically populates some fields when no explicit value is given. :param instance: The model instance to log a change for. :type instance: Model + :param force_log: Create a LogEntry even if no changes exist. + :type force_log: bool :param kwargs: Field overrides for the :py:class:`LogEntry` object. :return: The new log entry or `None` if there were no changes. :rtype: LogEntry @@ -42,7 +44,7 @@ def log_create(self, instance, **kwargs): changes = kwargs.get("changes", None) pk = self._get_pk_value(instance) - if changes is not None: + if changes is not None or force_log: kwargs.setdefault( "content_type", ContentType.objects.get_for_model(instance) ) @@ -330,7 +332,7 @@ class Action: action = models.PositiveSmallIntegerField( choices=Action.choices, verbose_name=_("action"), db_index=True ) - changes = models.TextField(blank=True, verbose_name=_("change message")) + changes = models.JSONField(null=True, verbose_name=_("change message")) actor = models.ForeignKey( to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, @@ -384,10 +386,7 @@ def changes_dict(self): """ :return: The changes recorded in this log entry as a dictionary object. """ - try: - return json.loads(self.changes) or {} - except ValueError: - return {} + return self.changes or {} @property def changes_str(self, colon=": ", arrow=" \u2192 ", separator="; "): diff --git a/auditlog/receivers.py b/auditlog/receivers.py index a78da458..c75619d7 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -1,4 +1,3 @@ -import json from functools import wraps from django.conf import settings @@ -115,7 +114,8 @@ def _create_log_entry( LogEntry.objects.log_create( instance, action=action, - changes=json.dumps(changes), + changes=changes, + force_log=force_log, ) except BaseException as e: error = e diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index f51ef05e..3f20fa66 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -105,9 +105,9 @@ def update(self, obj): obj.save() def check_update_log_entry(self, obj, history): - self.assertJSONEqual( + self.assertDictEqual( history.changes, - '{"boolean": ["False", "True"]}', + {"boolean": ["False", "True"]}, msg="The change is correctly logged", ) @@ -120,9 +120,9 @@ def test_update_specific_field_supplied_via_save_method(self): obj.save(update_fields=["boolean"]) # This implicitly asserts there is only one UPDATE change since the `.get` would fail otherwise. - self.assertJSONEqual( + self.assertDictEqual( obj.history.get(action=LogEntry.Action.UPDATE).changes, - '{"boolean": ["False", "True"]}', + {"boolean": ["False", "True"]}, msg=( "Object modifications that are not saved to DB are not logged " "when using the `update_fields`." @@ -153,9 +153,9 @@ def test_django_update_fields_edge_cases(self): obj.integer = 1 obj.boolean = True obj.save(update_fields=None) - self.assertJSONEqual( + self.assertDictEqual( obj.history.get(action=LogEntry.Action.UPDATE).changes, - '{"boolean": ["False", "True"], "integer": ["None", "1"]}', + {"boolean": ["False", "True"], "integer": ["None", "1"]}, msg="The 2 fields changed are correctly logged", ) @@ -537,9 +537,9 @@ def test_specified_save_fields_are_ignored_if_not_included(self): obj.text = "Newer text" obj.save(update_fields=["text", "label"]) - self.assertJSONEqual( + self.assertDictEqual( obj.history.get(action=LogEntry.Action.UPDATE).changes, - '{"label": ["Initial label", "New label"]}', + {"label": ["Initial label", "New label"]}, msg="Only the label was logged, regardless of multiple entries in `update_fields`", ) @@ -1411,7 +1411,27 @@ def _create_log_entry(self, action, changes): return LogEntry.objects.log_create( SimpleModel.objects.create(), # doesn't affect anything action=action, - changes=json.dumps(changes), + changes=changes, + ) + + def test_change_msg_create_when_exceeds_max_len(self): + log_entry = self._create_log_entry( + LogEntry.Action.CREATE, + { + "Camelopardalis": [None, "Giraffe"], + "Capricornus": [None, "Sea goat"], + "Equuleus": [None, "Little horse"], + "Horologium": [None, "Clock"], + "Microscopium": [None, "Microscope"], + "Reticulum": [None, "Net"], + "Telescopium": [None, "Telescope"], + }, + ) + + self.assertEqual( + self.admin.msg_short(log_entry), + "7 changes: Camelopardalis, Capricornus, Equuleus, Horologium, " + "Microscopium, ..", ) def test_changes_msg_delete(self): @@ -1585,9 +1605,9 @@ def test_update(self): history = obj.history.get(action=LogEntry.Action.UPDATE) - self.assertJSONEqual( + self.assertDictEqual( history.changes, - '{"json": ["{}", "{\'quantity\': \'1\'}"]}', + {"json": ["{}", "{'quantity': '1'}"]}, msg="The change is correctly logged", ) @@ -1926,7 +1946,7 @@ def test_access_log(self): self.assertEqual( log_entry.action, LogEntry.Action.ACCESS, msg="Action is 'ACCESS'" ) - self.assertEqual(log_entry.changes, "null") + self.assertIsNone(log_entry.changes) self.assertEqual(log_entry.changes_dict, {}) From eebde3a5e1f5d413325f5fd69da9aa3651b2a23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aivars=20Kalv=C4=81ns?= Date: Thu, 29 Dec 2022 10:09:56 +0200 Subject: [PATCH 077/126] Convert AUDITLOG_EXCLUDE_TRACKING_MODELS to tuple before concatenate (#488) --- auditlog/registry.py | 3 ++- auditlog_tests/tests.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/auditlog/registry.py b/auditlog/registry.py index 38e22a48..bd4b3285 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -248,7 +248,8 @@ def _get_exclude_models( ) -> List[ModelBase]: exclude_models = [ model - for app_model in exclude_tracking_models + self.DEFAULT_EXCLUDE_MODELS + for app_model in tuple(exclude_tracking_models) + + self.DEFAULT_EXCLUDE_MODELS for model in self._get_model_classes(app_model) ] return exclude_models diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 3f20fa66..f76502ed 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1168,7 +1168,17 @@ def test_register_from_settings_invalid_settings(self): AUDITLOG_INCLUDE_ALL_MODELS=True, AUDITLOG_EXCLUDE_TRACKING_MODELS=("auditlog_tests.SimpleExcludeModel",), ) - def test_register_from_settings_register_all_models_with_exclude_models(self): + def test_register_from_settings_register_all_models_with_exclude_models_tuple(self): + self.test_auditlog.register_from_settings() + + self.assertFalse(self.test_auditlog.contains(SimpleExcludeModel)) + self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel)) + + @override_settings( + AUDITLOG_INCLUDE_ALL_MODELS=True, + AUDITLOG_EXCLUDE_TRACKING_MODELS=["auditlog_tests.SimpleExcludeModel"], + ) + def test_register_from_settings_register_all_models_with_exclude_models_list(self): self.test_auditlog.register_from_settings() self.assertFalse(self.test_auditlog.contains(SimpleExcludeModel)) From f6a1c9c10c73feeef7c03ac147adb818dc9ef9b1 Mon Sep 17 00:00:00 2001 From: Cleiton de Lima Date: Fri, 30 Dec 2022 04:28:55 -0300 Subject: [PATCH 078/126] Fix repr of a json field in field changes (#489) --- CHANGELOG.md | 1 + auditlog/diff.py | 2 ++ auditlog_tests/models.py | 3 ++- auditlog_tests/tests.py | 30 +++++++++++++++++++++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7bf756..34eab63e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ #### Improvements +- Changes the view when it has changes in fields `JSONField`. The `JSONField.encoder` is assigned to `json.dumps`. ([#489](https://github.com/jazzband/django-auditlog/pull/489)) - feat: Added support for Correlation ID. ([#481](https://github.com/jazzband/django-auditlog/pull/481)) - feat: Added pre-log and post-log signals. ([#483](https://github.com/jazzband/django-auditlog/pull/483)) diff --git a/auditlog/diff.py b/auditlog/diff.py index 0d25c861..7f4daae1 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -1,3 +1,4 @@ +import json from datetime import timezone from typing import Optional @@ -74,6 +75,7 @@ def get_field_value(obj, field): value = django_timezone.make_naive(value, timezone=timezone.utc) elif isinstance(field, JSONField): value = field.to_python(getattr(obj, field.name, None)) + value = json.dumps(value, sort_keys=True, cls=field.encoder) else: value = smart_str(getattr(obj, field.name, None)) except ObjectDoesNotExist: diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index 02f022cb..d7fb7ce0 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -1,6 +1,7 @@ import uuid from django.contrib.postgres.fields import ArrayField +from django.core.serializers.json import DjangoJSONEncoder from django.db import models from auditlog.models import AuditlogHistoryField @@ -257,7 +258,7 @@ class NoDeleteHistoryModel(models.Model): class JSONModel(models.Model): - json = models.JSONField(default=dict) + json = models.JSONField(default=dict, encoder=DjangoJSONEncoder) history = AuditlogHistoryField(delete_related=False) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index f76502ed..49e1c525 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1617,7 +1617,7 @@ def test_update(self): self.assertDictEqual( history.changes, - {"json": ["{}", "{'quantity': '1'}"]}, + {"json": ["{}", '{"quantity": "1"}']}, msg="The change is correctly logged", ) @@ -1709,6 +1709,34 @@ def test_when_field_doesnt_exist(self): msg="ObjectDoesNotExist should be handled", ) + def test_diff_models_with_json_fields(self): + first = JSONModel.objects.create( + json={ + "code": "17", + "date": datetime.date(2022, 1, 1), + "description": "first", + } + ) + first.refresh_from_db() # refresh json data from db + second = JSONModel.objects.create( + json={ + "code": "17", + "description": "second", + "date": datetime.date(2023, 1, 1), + } + ) + diff = model_instance_diff(first, second, ["json"]) + + self.assertDictEqual( + diff, + { + "json": ( + '{"code": "17", "date": "2022-01-01", "description": "first"}', + '{"code": "17", "date": "2023-01-01", "description": "second"}', + ) + }, + ) + class TestModelSerialization(TestCase): def setUp(self): From 60760ee068319252e1ba43746f3593397aef2e5e Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Tue, 3 Jan 2023 16:03:37 +0100 Subject: [PATCH 079/126] raise AuditLogRegistrationError on invalid app in settings (#492) Co-authored-by: Hasan Ramezani --- auditlog/registry.py | 8 +++++++- auditlog_tests/tests.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/auditlog/registry.py b/auditlog/registry.py index bd4b3285..17ec4d5f 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -262,7 +262,13 @@ def _register_models(self, models: Iterable[Union[str, Dict[str, Any]]]) -> None self.unregister(model_class) self.register(model_class) elif isinstance(model, dict): - model["model"] = self._get_model_classes(model["model"])[0] + appmodel = self._get_model_classes(model["model"]) + if not appmodel: + raise AuditLogRegistrationError( + f"An error was encountered while registering model '{model['model']}' - " + "make sure the app is registered correctly." + ) + model["model"] = appmodel[0] self.unregister(model["model"]) self.register(**model) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 49e1c525..48d69433 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1158,6 +1158,18 @@ def test_register_from_settings_invalid_settings(self): ): self.test_auditlog.register_from_settings() + with override_settings( + AUDITLOG_INCLUDE_TRACKING_MODELS=({"model": "notanapp.test"},) + ): + with self.assertRaisesMessage( + AuditLogRegistrationError, + ( + "An error was encountered while registering model 'notanapp.test'" + " - make sure the app is registered correctly." + ), + ): + self.test_auditlog.register_from_settings() + with override_settings(AUDITLOG_DISABLE_ON_RAW_SAVE="bad value"): with self.assertRaisesMessage( TypeError, "Setting 'AUDITLOG_DISABLE_ON_RAW_SAVE' must be a boolean" From abb9c25ae519b0bcba8c3fd129e7563f4c0cca58 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Wed, 4 Jan 2023 17:05:32 +0300 Subject: [PATCH 080/126] Always call set_actor (#484) --- CHANGELOG.md | 13 +++++-------- auditlog/context.py | 9 ++------- auditlog/middleware.py | 17 ++++++++++------- auditlog_tests/tests.py | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34eab63e..5c4de4c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,10 @@ # Changes -#### Breaking Changes - -- feat: Change `LogEntry.change` field type to `JSONField` rather than `TextField`. This change include a migration that may take time to run depending on the number of records on your `LogEntry` table ([#407](https://github.com/jazzband/django-auditlog/pull/407)) - ## Next Release #### Breaking Changes +- feat: Change `LogEntry.change` field type to `JSONField` rather than `TextField`. This change include a migration that may take time to run depending on the number of records on your `LogEntry` table ([#407](https://github.com/jazzband/django-auditlog/pull/407)) - feat: stop deleting old log entries when a model with the same pk is created (i.e. the pk value is reused) ([#559](https://github.com/jazzband/django-auditlog/pull/559)) - feat: Set `AuditlogHistoryField.delete_related` to `False` by default. This is different from the default configuration of Django's `GenericRelation`, but we should not erase the audit log of objects on deletion by default. ([#557](https://github.com/jazzband/django-auditlog/pull/557)) - Python: Drop support for Python 3.7 ([#546](https://github.com/jazzband/django-auditlog/pull/546)) @@ -17,18 +14,18 @@ - Changes the view when it has changes in fields `JSONField`. The `JSONField.encoder` is assigned to `json.dumps`. ([#489](https://github.com/jazzband/django-auditlog/pull/489)) - feat: Added support for Correlation ID. ([#481](https://github.com/jazzband/django-auditlog/pull/481)) - feat: Added pre-log and post-log signals. ([#483](https://github.com/jazzband/django-auditlog/pull/483)) +- feat: Make timestamp in LogEntry overwritable. ([#476](https://github.com/jazzband/django-auditlog/pull/476)) #### Fixes - fix: Make sure `LogEntry.changes_dict()` returns an empty dict instead of `None` when `json.loads()` returns `None`. ([#472](https://github.com/jazzband/django-auditlog/pull/472)) -- feat: Make timestamp in LogEntry overwritable. ([#476](https://github.com/jazzband/django-auditlog/pull/476)) - fix: Make log entries read-only in the admin. ([#449](https://github.com/jazzband/django-auditlog/pull/449), [#556](https://github.com/jazzband/django-auditlog/pull/556)) (applied again after being reverted in 2.2.2) +~~- fix: revert [#449](https://github.com/jazzband/django-auditlog/pull/449) "Make log entries read-only in the admin" as it breaks deletion of any auditlogged model through the admin when `AuditlogHistoryField` is used. ([#496](https://github.com/jazzband/django-auditlog/pull/496))~~ +- fix: Always set remote_addr even if the request has no authenticated user. ([#484](https://github.com/jazzband/django-auditlog/pull/484)) ## 2.2.2 (2023-01-16) -#### Fixes - -~~- fix: revert [#449](https://github.com/jazzband/django-auditlog/pull/449) "Make log entries read-only in the admin" as it breaks deletion of any auditlogged model through the admin when `AuditlogHistoryField` is used. ([#496](https://github.com/jazzband/django-auditlog/pull/496))~~ +No changes. ## 2.2.1 (2022-11-28) diff --git a/auditlog/context.py b/auditlog/context.py index de2c0cad..80d2fae5 100644 --- a/auditlog/context.py +++ b/auditlog/context.py @@ -3,7 +3,6 @@ import time from functools import partial -from django.contrib.auth import get_user_model from django.db.models.signals import pre_save from auditlog.models import LogEntry @@ -55,12 +54,8 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs): 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 - ): + + if sender == LogEntry and instance.actor is None: instance.actor = user instance.remote_addr = auditlog["remote_addr"] diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 9d07ed79..e3274ee1 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,4 +1,4 @@ -import contextlib +from django.contrib.auth import get_user_model from auditlog.cid import set_cid from auditlog.context import set_actor @@ -30,15 +30,18 @@ def _get_remote_addr(request): return remote_addr + @staticmethod + def _get_actor(request): + user = getattr(request, "user", None) + if isinstance(user, get_user_model()) and user.is_authenticated: + return user + return None + def __call__(self, request): remote_addr = self._get_remote_addr(request) + user = self._get_actor(request) set_cid(request) - if hasattr(request, "user") and request.user.is_authenticated: - context = set_actor(actor=request.user, remote_addr=remote_addr) - else: - context = contextlib.nullcontext() - - with context: + with set_actor(actor=user, remote_addr=remote_addr): return self.get_response(request) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 48d69433..308c18f0 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -433,7 +433,7 @@ def test_request_anonymous(self): request = self.factory.get("/") request.user = AnonymousUser() - self.get_response_mock.side_effect = self.side_effect(self.assert_no_listeners) + self.get_response_mock.side_effect = self.side_effect(self.assert_has_listeners) response = self.middleware(request) @@ -518,6 +518,38 @@ def test_cid(self): self.assertEqual(history.cid, expected_result) self.assertEqual(get_cid(), expected_result) + def test_set_actor_anonymous_request(self): + """ + The remote address will be set even when there is no actor + """ + remote_addr = "123.213.145.99" + actor = None + + with set_actor(actor=actor, remote_addr=remote_addr): + obj = SimpleModel.objects.create(text="I am not difficult.") + + history = obj.history.get() + self.assertEqual( + history.remote_addr, + remote_addr, + msg=f"Remote address is {remote_addr}", + ) + self.assertIsNone(history.actor, msg="Actor is `None` for anonymous user") + + def test_get_actor(self): + params = [ + (AnonymousUser(), None, "The user is anonymous so the actor is `None`"), + (self.user, self.user, "The use is authenticated so it is the actor"), + (None, None, "There is no actor"), + ("1234", None, "The value of request.user is not a valid user model"), + ] + for user, actor, msg in params: + with self.subTest(msg): + request = self.factory.get("/") + request.user = user + + self.assertEqual(self.middleware._get_actor(request), actor) + class SimpleIncludeModelTest(TestCase): """Log only changes in include_fields""" From df4922e3a2769f9bc80d84ccaa524d62640084b8 Mon Sep 17 00:00:00 2001 From: "Aaron C. de Bruyn" Date: Fri, 20 Jan 2023 06:41:36 -0800 Subject: [PATCH 081/126] Add ability to globally exclude fields by name on all models (#498) Co-authored-by: Hasan Ramezani --- CHANGELOG.md | 1 + auditlog/conf.py | 5 +++++ auditlog/registry.py | 17 +++++++++++++++++ auditlog_tests/tests.py | 38 ++++++++++++++++++++++++++++++++++++++ docs/source/usage.rst | 17 +++++++++++++++++ 5 files changed, 78 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c4de4c8..52fb88e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - feat: Added support for Correlation ID. ([#481](https://github.com/jazzband/django-auditlog/pull/481)) - feat: Added pre-log and post-log signals. ([#483](https://github.com/jazzband/django-auditlog/pull/483)) - feat: Make timestamp in LogEntry overwritable. ([#476](https://github.com/jazzband/django-auditlog/pull/476)) +- feat: Support excluding field names globally when ```AUDITLOG_INCLUDE_ALL_MODELS``` is enabled. ([#498](https://github.com/jazzband/django-auditlog/pull/498)) #### Fixes diff --git a/auditlog/conf.py b/auditlog/conf.py index a56a165e..2050677a 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -16,6 +16,11 @@ settings, "AUDITLOG_INCLUDE_TRACKING_MODELS", () ) +# Exclude named fields across all models +settings.AUDITLOG_EXCLUDE_TRACKING_FIELDS = getattr( + settings, "AUDITLOG_EXCLUDE_TRACKING_FIELDS", () +) + # Disable on raw save to avoid logging imports and similar settings.AUDITLOG_DISABLE_ON_RAW_SAVE = getattr( settings, "AUDITLOG_DISABLE_ON_RAW_SAVE", False diff --git a/auditlog/registry.py b/auditlog/registry.py index 17ec4d5f..139f3df3 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -113,6 +113,9 @@ def register( "set. Did you forget to set serialized_data to True?" ) + for fld in settings.AUDITLOG_EXCLUDE_TRACKING_FIELDS: + exclude_fields.append(fld) + def registrar(cls): """Register models for a given class.""" if not issubclass(cls, Model): @@ -294,11 +297,25 @@ def register_from_settings(self): "setting 'AUDITLOG_INCLUDE_ALL_MODELS' must set to 'True'" ) + if ( + settings.AUDITLOG_EXCLUDE_TRACKING_FIELDS + and not settings.AUDITLOG_INCLUDE_ALL_MODELS + ): + raise ValueError( + "In order to use 'AUDITLOG_EXCLUDE_TRACKING_FIELDS', " + "setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be set to 'True'" + ) + if not isinstance(settings.AUDITLOG_INCLUDE_TRACKING_MODELS, (list, tuple)): raise TypeError( "Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' must be a list or tuple" ) + if not isinstance(settings.AUDITLOG_EXCLUDE_TRACKING_FIELDS, (list, tuple)): + raise TypeError( + "Setting 'AUDITLOG_EXCLUDE_TRACKING_FIELDS' must be a list or tuple" + ) + for item in settings.AUDITLOG_INCLUDE_TRACKING_MODELS: if not isinstance(item, (str, dict)): raise TypeError( diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 308c18f0..a2d44ae9 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1159,6 +1159,26 @@ def test_register_from_settings_invalid_settings(self): ): self.test_auditlog.register_from_settings() + with override_settings( + AUDITLOG_INCLUDE_ALL_MODELS=True, + AUDITLOG_EXCLUDE_TRACKING_FIELDS="badvalue", + ): + with self.assertRaisesMessage( + TypeError, + "Setting 'AUDITLOG_EXCLUDE_TRACKING_FIELDS' must be a list or tuple", + ): + self.test_auditlog.register_from_settings() + + with override_settings( + AUDITLOG_EXCLUDE_TRACKING_FIELDS=("created", "modified") + ): + with self.assertRaisesMessage( + ValueError, + "In order to use 'AUDITLOG_EXCLUDE_TRACKING_FIELDS', " + "setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be set to 'True'", + ): + self.test_auditlog.register_from_settings() + with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS="str"): with self.assertRaisesMessage( TypeError, @@ -1218,6 +1238,24 @@ def test_register_from_settings_register_all_models_with_exclude_models_tuple(se self.assertFalse(self.test_auditlog.contains(SimpleExcludeModel)) self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel)) + @override_settings( + AUDITLOG_INCLUDE_ALL_MODELS=True, + AUDITLOG_EXCLUDE_TRACKING_FIELDS=("datetime",), + ) + def test_register_from_settings_register_all_models_with_exclude_tracking_fields( + self, + ): + self.test_auditlog.register_from_settings() + + self.assertEqual( + self.test_auditlog.get_model_fields(SimpleModel)["exclude_fields"], + ["datetime"], + ) + self.assertEqual( + self.test_auditlog.get_model_fields(AltPrimaryKeyModel)["exclude_fields"], + ["datetime"], + ) + @override_settings( AUDITLOG_INCLUDE_ALL_MODELS=True, AUDITLOG_EXCLUDE_TRACKING_MODELS=["auditlog_tests.SimpleExcludeModel"], diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 4961be2d..376737a3 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -189,6 +189,23 @@ You can use this setting to register all your models: .. versionadded:: 2.1.0 +**AUDITLOG_EXCLUDE_TRACKING_FIELDS** + +You can use this setting to exclude named fields from ALL models. +This is useful when lots of models share similar fields like +```created``` and ```modified``` and you want those excluded from +logging. +It will be considered when ``AUDITLOG_INCLUDE_ALL_MODELS`` is `True`. + +.. code-block:: python + + AUDITLOG_EXCLUDE_TRACKING_FIELDS = ( + "created", + "modified" + ) + +.. versionadded:: 3.0.0 + **AUDITLOG_EXCLUDE_TRACKING_MODELS** You can use this setting to exclude models in registration process. From 02aeaea71a88e2a68875466063bf1b8d6072183a Mon Sep 17 00:00:00 2001 From: Joey Lange Date: Wed, 15 Feb 2023 08:10:27 -0600 Subject: [PATCH 082/126] Make M2M changes comply with JSONField properly (#514) --- auditlog/models.py | 15 +++++++-------- auditlog_tests/tests.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/auditlog/models.py b/auditlog/models.py index 5ccb960f..be490981 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -102,15 +102,14 @@ def log_m2m_changes( kwargs.setdefault("additional_data", get_additional_data()) objects = [smart_str(instance) for instance in changed_queryset] - kwargs["changes"] = json.dumps( - { - field_name: { - "type": "m2m", - "operation": operation, - "objects": objects, - } + kwargs["changes"] = { + field_name: { + "type": "m2m", + "operation": operation, + "objects": objects, } - ) + } + kwargs.setdefault("cid", get_cid()) return self.create(**kwargs) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index a2d44ae9..4e11c320 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -21,6 +21,7 @@ from django.urls import resolve, reverse from django.utils import dateformat, formats from django.utils import timezone as django_timezone +from django.utils.encoding import smart_str from auditlog.admin import LogEntryAdmin from auditlog.cid import get_cid @@ -400,6 +401,20 @@ def test_additional_data(self): log_entry.additional_data, {"related_model_id": self.related.id} ) + def test_changes(self): + self.obj.related.add(self.related) + log_entry = self.obj.history.first() + self.assertEqual( + log_entry.changes, + { + "related": { + "type": "m2m", + "operation": "add", + "objects": [smart_str(self.related)], + } + }, + ) + class MiddlewareTest(TestCase): """ From 3c860f41cfa847356659079f37f0134d121c4545 Mon Sep 17 00:00:00 2001 From: Rustam Astafeev Date: Thu, 18 May 2023 03:29:11 +0500 Subject: [PATCH 083/126] fixed getting field's verbose_name (#508) Co-authored-by: Hasan Ramezani --- CHANGELOG.md | 1 + auditlog/mixins.py | 2 ++ auditlog_tests/tests.py | 13 +++++++++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fb88e8..663ed600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - fix: Make log entries read-only in the admin. ([#449](https://github.com/jazzband/django-auditlog/pull/449), [#556](https://github.com/jazzband/django-auditlog/pull/556)) (applied again after being reverted in 2.2.2) ~~- fix: revert [#449](https://github.com/jazzband/django-auditlog/pull/449) "Make log entries read-only in the admin" as it breaks deletion of any auditlogged model through the admin when `AuditlogHistoryField` is used. ([#496](https://github.com/jazzband/django-auditlog/pull/496))~~ - fix: Always set remote_addr even if the request has no authenticated user. ([#484](https://github.com/jazzband/django-auditlog/pull/484)) +- fix: Fix a bug in getting field's `verbose_name` when model is not accessible. ([508](https://github.com/jazzband/django-auditlog/pull/508)) ## 2.2.2 (2023-01-16) diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 0cdd5db7..422e0970 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -177,6 +177,8 @@ def _get_timezone_warning(self): def field_verbose_name(self, obj, field_name: str): model = obj.content_type.model_class() + if model is None: + return field_name try: model_fields = auditlog.get_model_fields(model._meta.model) mapping_field_name = model_fields["mapping_fields"].get(field_name) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 4e11c320..1ee01eac 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1656,6 +1656,19 @@ def test_unregister_after_log(self): # Re-register auditlog.register(SimpleModel) + def test_field_verbose_name(self): + log_entry = self._create_log_entry( + LogEntry.Action.CREATE, + {"test": "test"}, + ) + + self.assertEqual(self.admin.field_verbose_name(log_entry, "actor"), "Actor") + with patch( + "django.contrib.contenttypes.models.ContentType.model_class", + return_value=None, + ): + self.assertEqual(self.admin.field_verbose_name(log_entry, "actor"), "actor") + class NoDeleteHistoryTest(TestCase): def test_delete_related(self): From f35d8f04e7056a647043cfce9b3bcfea51207086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C4=B1rat=20K=C4=B1l=C4=B1=C3=A7?= <22756097+firtk@users.noreply.github.com> Date: Wed, 21 Jun 2023 17:25:29 +0300 Subject: [PATCH 084/126] Fix unnecessary log when adding already existed m2m record (#535) --- auditlog/models.py | 2 +- auditlog_tests/tests.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/auditlog/models.py b/auditlog/models.py index be490981..933b09e4 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -86,7 +86,7 @@ def log_m2m_changes( from auditlog.cid import get_cid pk = self._get_pk_value(instance) - if changed_queryset is not None: + if changed_queryset: kwargs.setdefault( "content_type", ContentType.objects.get_for_model(instance) ) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 1ee01eac..a79568ac 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -415,6 +415,24 @@ def test_changes(self): }, ) + def test_adding_existing_related_obj(self): + self.obj.related.add(self.related) + log_entry = self.obj.history.first() + self.assertEqual( + log_entry.changes, + { + "related": { + "type": "m2m", + "operation": "add", + "objects": [smart_str(self.related)], + } + }, + ) + # Add same related obj again. + self.obj.related.add(self.related) + latest_log_entry = self.obj.history.first() + self.assertEqual(log_entry.id, latest_log_entry.id) + class MiddlewareTest(TestCase): """ From 1949b626568de51613b08a5cba694787dadc2ec5 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 13 Jul 2023 08:53:47 +0200 Subject: [PATCH 085/126] Fix a bug in `serialized_data` with F expressions (#544) --- CHANGELOG.md | 1 + auditlog/models.py | 5 ++++- auditlog_tests/tests.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 663ed600..b954178d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ ~~- fix: revert [#449](https://github.com/jazzband/django-auditlog/pull/449) "Make log entries read-only in the admin" as it breaks deletion of any auditlogged model through the admin when `AuditlogHistoryField` is used. ([#496](https://github.com/jazzband/django-auditlog/pull/496))~~ - fix: Always set remote_addr even if the request has no authenticated user. ([#484](https://github.com/jazzband/django-auditlog/pull/484)) - fix: Fix a bug in getting field's `verbose_name` when model is not accessible. ([508](https://github.com/jazzband/django-auditlog/pull/508)) +- fix: Fix a bug in `serialized_data` with F expressions. ([508](https://github.com/jazzband/django-auditlog/pull/508)) ## 2.2.2 (2023-01-16) diff --git a/auditlog/models.py b/auditlog/models.py index 933b09e4..1f2645b4 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -249,7 +249,10 @@ def _get_copy_with_python_typed_fields(self, instance): for field in instance_copy._meta.fields: if not field.is_relation: value = getattr(instance_copy, field.name) - setattr(instance_copy, field.name, field.to_python(value)) + try: + setattr(instance_copy, field.name, field.to_python(value)) + except ValidationError: + continue return instance_copy def _get_applicable_model_fields( diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index a79568ac..75a4e0a4 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -16,6 +16,7 @@ from django.contrib.auth.models import AnonymousUser, User from django.contrib.contenttypes.models import ContentType from django.core import management +from django.db import models from django.db.models.signals import pre_save from django.test import RequestFactory, TestCase, override_settings from django.urls import resolve, reverse @@ -2087,6 +2088,24 @@ def test_serialize_related_with_kwargs(self): }, ) + def test_f_expressions(self): + serialize_this = SerializeThisModel.objects.create( + label="test label", + nested={"foo": "bar"}, + timestamp=self.test_date, + nullable=1, + ) + serialize_this.nullable = models.F("nullable") + 1 + serialize_this.save() + + log = serialize_this.history.first() + self.assertTrue(isinstance(log, LogEntry)) + self.assertEqual(log.action, 1) + self.assertEqual( + log.serialized_data["fields"]["nullable"], + "F(nullable) + Value(1)", + ) + class TestAccessLog(TestCase): def setUp(self): From e5e1e3bf8277ab918b5fe6fe003df12d71a5e26e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 23:44:44 +0200 Subject: [PATCH 086/126] [pre-commit.ci] pre-commit autoupdate (#547) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f12cc6ee..fb253d2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black language_version: python3.8 From dc5edf5fc69cf2a567991929058c2f3b358e3ad7 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Wed, 9 Aug 2023 18:11:21 +0300 Subject: [PATCH 087/126] feat: collect all models including auto created ones and excluding non-managed ones (#550) feat: automatically register m2m fields for models when opting to auto register models --- CHANGELOG.md | 1 + auditlog/registry.py | 24 ++++++++++++++++++++---- auditlog_tests/models.py | 23 +++++++++++++++++++++++ auditlog_tests/tests.py | 30 +++++++++++++++++++++++++++++- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b954178d..ad15847b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - feat: Added pre-log and post-log signals. ([#483](https://github.com/jazzband/django-auditlog/pull/483)) - feat: Make timestamp in LogEntry overwritable. ([#476](https://github.com/jazzband/django-auditlog/pull/476)) - feat: Support excluding field names globally when ```AUDITLOG_INCLUDE_ALL_MODELS``` is enabled. ([#498](https://github.com/jazzband/django-auditlog/pull/498)) +- feat: Improved auto model registration to include auto-created models and exclude non-managed models, and automatically register m2m fields for models. ([#550](https://github.com/jazzband/django-auditlog/pull/550)) #### Fixes diff --git a/auditlog/registry.py b/auditlog/registry.py index 139f3df3..7dfb15f7 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -13,7 +13,7 @@ ) from django.apps import apps -from django.db.models import Model +from django.db.models import ManyToManyField, Model from django.db.models.base import ModelBase from django.db.models.signals import ( ModelSignal, @@ -337,12 +337,28 @@ def register_from_settings(self): exclude_models = self._get_exclude_models( settings.AUDITLOG_EXCLUDE_TRACKING_MODELS ) - models = apps.get_models() - for model in models: + for model in apps.get_models(include_auto_created=True): if model in exclude_models: continue - self.register(model) + + meta = model._meta + if not meta.managed: + continue + + m2m_fields = [ + m.name for m in meta.get_fields() if isinstance(m, ManyToManyField) + ] + + exclude_fields = [ + i.related_name + for i in meta.related_objects + if i.related_name and not i.related_model._meta.managed + ] + + self.register( + model=model, m2m_fields=m2m_fields, exclude_fields=exclude_fields + ) self._register_models(settings.AUDITLOG_INCLUDE_TRACKING_MODELS) diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index d7fb7ce0..3e0678b5 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -301,6 +301,29 @@ class SerializeNaturalKeyRelatedModel(models.Model): history = AuditlogHistoryField(delete_related=False) +class SimpleNonManagedModel(models.Model): + """ + A simple model with no special things going on. + """ + + text = models.TextField(blank=True) + boolean = models.BooleanField(default=False) + integer = models.IntegerField(blank=True, null=True) + datetime = models.DateTimeField(auto_now=True) + + history = AuditlogHistoryField() + + def __str__(self): + return self.text + + class Meta: + managed = False + + +class AutoManyRelatedModel(models.Model): + related = models.ManyToManyField(SimpleModel) + + auditlog.register(AltPrimaryKeyModel) auditlog.register(UUIDPrimaryKeyModel) auditlog.register(ProxyModel) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 75a4e0a4..d121f0d4 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -36,6 +36,7 @@ from auditlog_tests.models import ( AdditionalDataIncludedModel, AltPrimaryKeyModel, + AutoManyRelatedModel, CharfieldTextfieldModel, ChoicesFieldModel, DateTimeFieldModel, @@ -55,6 +56,7 @@ SimpleMappingModel, SimpleMaskedModel, SimpleModel, + SimpleNonManagedModel, UUIDPrimaryKeyModel, ) @@ -1136,7 +1138,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()), 23) + self.assertEqual(len(self.test_auditlog.get_models()), 25) def test_register_models_register_model_with_attrs(self): self.test_auditlog._register_models( @@ -1330,6 +1332,32 @@ def test_registration_error_if_bad_serialize_params(self): SimpleModel, serialize_kwargs={"fields": ["text", "integer"]} ) + @override_settings(AUDITLOG_INCLUDE_ALL_MODELS=True) + def test_register_from_settings_register_all_models_excluding_non_managed_models( + self, + ): + self.test_auditlog.register_from_settings() + + self.assertFalse(self.test_auditlog.contains(SimpleNonManagedModel)) + + @override_settings(AUDITLOG_INCLUDE_ALL_MODELS=True) + def test_register_from_settings_register_all_models_and_figure_out_m2m_fields(self): + self.test_auditlog.register_from_settings() + + self.assertIn( + "related", self.test_auditlog._registry[AutoManyRelatedModel]["m2m_fields"] + ) + + @override_settings(AUDITLOG_INCLUDE_ALL_MODELS=True) + def test_register_from_settings_register_all_models_including_auto_created_models( + self, + ): + self.test_auditlog.register_from_settings() + + self.assertTrue( + self.test_auditlog.contains(AutoManyRelatedModel.related.through) + ) + class ChoicesFieldModelTest(TestCase): def setUp(self): From 3c6d86fb2b0f52843e7b3eefad24d6ab1e643490 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Sun, 13 Aug 2023 12:38:21 +0300 Subject: [PATCH 088/126] feat: give users the option to run the json migration asyncly (#495) --- CHANGELOG.md | 2 +- auditlog/apps.py | 4 + auditlog/conf.py | 8 + .../commands/auditlogmigratejson.py | 114 +++++++++++++ .../migrations/0017_alter_logentry_changes.py | 36 ++++- auditlog/models.py | 35 +++- .../test_two_step_json_migration.py | 151 ++++++++++++++++++ docs/source/index.rst | 1 + docs/source/upgrade.rst | 21 +++ 9 files changed, 359 insertions(+), 13 deletions(-) create mode 100644 auditlog/management/commands/auditlogmigratejson.py create mode 100644 auditlog_tests/test_two_step_json_migration.py create mode 100644 docs/source/upgrade.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index ad15847b..e22debfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ #### Breaking Changes -- feat: Change `LogEntry.change` field type to `JSONField` rather than `TextField`. This change include a migration that may take time to run depending on the number of records on your `LogEntry` table ([#407](https://github.com/jazzband/django-auditlog/pull/407)) +- feat: Change `LogEntry.change` field type to `JSONField` rather than `TextField`. This change include a migration that may take time to run depending on the number of records on your `LogEntry` table ([#407](https://github.com/jazzband/django-auditlog/pull/407))([#495](https://github.com/jazzband/django-auditlog/pull/495)) - feat: stop deleting old log entries when a model with the same pk is created (i.e. the pk value is reused) ([#559](https://github.com/jazzband/django-auditlog/pull/559)) - feat: Set `AuditlogHistoryField.delete_related` to `False` by default. This is different from the default configuration of Django's `GenericRelation`, but we should not erase the audit log of objects on deletion by default. ([#557](https://github.com/jazzband/django-auditlog/pull/557)) - Python: Drop support for Python 3.7 ([#546](https://github.com/jazzband/django-auditlog/pull/546)) diff --git a/auditlog/apps.py b/auditlog/apps.py index f6bf3fbd..aaae2768 100644 --- a/auditlog/apps.py +++ b/auditlog/apps.py @@ -11,3 +11,7 @@ def ready(self): from auditlog.registry import auditlog auditlog.register_from_settings() + + from auditlog import models + + models.changes_func = models._changes_func() diff --git a/auditlog/conf.py b/auditlog/conf.py index 2050677a..9046669a 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -32,3 +32,11 @@ settings, "AUDITLOG_CID_HEADER", "x-correlation-id" ) settings.AUDITLOG_CID_GETTER = getattr(settings, "AUDITLOG_CID_GETTER", None) + +# migration +settings.AUDITLOG_TWO_STEP_MIGRATION = getattr( + settings, "AUDITLOG_TWO_STEP_MIGRATION", False +) +settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT = getattr( + settings, "AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT", False +) diff --git a/auditlog/management/commands/auditlogmigratejson.py b/auditlog/management/commands/auditlogmigratejson.py new file mode 100644 index 00000000..db27ef6c --- /dev/null +++ b/auditlog/management/commands/auditlogmigratejson.py @@ -0,0 +1,114 @@ +from math import ceil + +from django.conf import settings +from django.core.management.base import BaseCommand + +from auditlog.models import LogEntry + + +class Command(BaseCommand): + help = "Migrates changes from changes_text to json changes." + + def add_arguments(self, parser): + parser.add_argument( + "-d", + "--database", + default=None, + help="If provided, the script will use native db operations. " + "Otherwise, it will use LogEntry.objects.bulk_create", + dest="db", + type=str, + choices=["postgres", "mysql", "oracle"], + ) + parser.add_argument( + "-b", + "--bactch-size", + default=500, + help="Split the migration into multiple batches. If 0, then no batching will be done. " + "When passing a -d/database, the batch value will be ignored.", + dest="batch_size", + type=int, + ) + + def handle(self, *args, **options): + database = options["db"] + batch_size = options["batch_size"] + + if not self.check_logs(): + return + + if database: + result = self.migrate_using_sql(database) + self.stdout.write( + f"Updated {result} records using native database operations." + ) + else: + result = self.migrate_using_django(batch_size) + self.stdout.write(f"Updated {result} records using django operations.") + + self.check_logs() + + def check_logs(self): + count = self.get_logs().count() + if count: + self.stdout.write(f"There are {count} records that needs migration.") + return True + + self.stdout.write("All records are have been migrated.") + if settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT: + self.stdout.write( + "You can now set AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT to False." + ) + + return False + + def get_logs(self): + return LogEntry.objects.filter( + changes_text__isnull=False, changes__isnull=True + ).exclude(changes_text__exact="") + + def migrate_using_django(self, batch_size): + def _apply_django_migration(_logs) -> int: + import json + + updated = [] + for log in _logs: + try: + log.changes = json.loads(log.changes_text) + except ValueError: + self.stderr.write( + f"ValueError was raised while migrating the log with id {log.id}." + ) + else: + updated.append(log) + + LogEntry.objects.bulk_update(updated, fields=["changes"]) + return len(updated) + + logs = self.get_logs() + + if not batch_size: + return _apply_django_migration(logs) + + total_updated = 0 + for _ in range(ceil(logs.count() / batch_size)): + total_updated += _apply_django_migration(self.get_logs()[:batch_size]) + return total_updated + + def migrate_using_sql(self, database): + from django.db import connection + + def postgres(): + with connection.cursor() as cursor: + cursor.execute( + 'UPDATE auditlog_logentry SET changes="changes_text"::jsonb' + ) + return cursor.cursor.rowcount + + if database == "postgres": + return postgres() + else: + self.stderr.write( + "Not yet implemented. Run this management command without passing a -d/--database argument." + ) + return 0 diff --git a/auditlog/migrations/0017_alter_logentry_changes.py b/auditlog/migrations/0017_alter_logentry_changes.py index b8e685fb..a2324f28 100644 --- a/auditlog/migrations/0017_alter_logentry_changes.py +++ b/auditlog/migrations/0017_alter_logentry_changes.py @@ -1,18 +1,42 @@ # Generated by Django 4.0 on 2022-08-04 15:41 +from typing import List +from django.conf import settings from django.db import migrations, models -class Migration(migrations.Migration): +def two_step_migrations() -> List: + if settings.AUDITLOG_TWO_STEP_MIGRATION: + return [ + migrations.RenameField( + model_name="logentry", + old_name="changes", + new_name="changes_text", + ), + migrations.AddField( + model_name="logentry", + name="changes", + field=models.JSONField(null=True, verbose_name="change message"), + ), + ] - dependencies = [ - ("auditlog", "0016_logentry_cid"), - ] - - operations = [ + return [ + migrations.AddField( + model_name="logentry", + name="changes_text", + field=models.TextField(blank=True, verbose_name="text change message"), + ), migrations.AlterField( model_name="logentry", name="changes", field=models.JSONField(null=True, verbose_name="change message"), ), ] + + +class Migration(migrations.Migration): + dependencies = [ + ("auditlog", "0016_logentry_cid"), + ] + + operations = [*two_step_migrations()] diff --git a/auditlog/models.py b/auditlog/models.py index 1f2645b4..09086d0c 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -1,8 +1,9 @@ import ast +import contextlib import json from copy import deepcopy from datetime import timezone -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List from dateutil import parser from dateutil.tz import gettz @@ -10,7 +11,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from django.core import serializers -from django.core.exceptions import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist, ValidationError from django.db import DEFAULT_DB_ALIAS, models from django.db.models import Q, QuerySet from django.utils import formats @@ -201,7 +202,7 @@ def _get_pk_value(self, instance): pk_field = instance._meta.pk.name pk = getattr(instance, pk_field, None) - # Check to make sure that we got an pk not a model object. + # Check to make sure that we got a pk not a model object. if isinstance(pk, models.Model): pk = self._get_pk_value(pk) return pk @@ -334,6 +335,7 @@ class Action: action = models.PositiveSmallIntegerField( choices=Action.choices, verbose_name=_("action"), db_index=True ) + changes_text = models.TextField(blank=True, verbose_name=_("change message")) changes = models.JSONField(null=True, verbose_name=_("change message")) actor = models.ForeignKey( to=settings.AUTH_USER_MODEL, @@ -388,7 +390,7 @@ def changes_dict(self): """ :return: The changes recorded in this log entry as a dictionary object. """ - return self.changes or {} + return changes_func(self) @property def changes_str(self, colon=": ", arrow=" \u2192 ", separator="; "): @@ -436,7 +438,7 @@ def changes_display_dict(self): changes_display_dict[field_name] = values continue values_display = [] - # handle choices fields and Postgres ArrayField to get human readable version + # handle choices fields and Postgres ArrayField to get human-readable version choices_dict = None if getattr(field, "choices", []): choices_dict = dict(field.choices) @@ -495,7 +497,7 @@ class AuditlogHistoryField(GenericRelation): A subclass of py:class:`django.contrib.contenttypes.fields.GenericRelation` that sets some default variables. This makes it easier to access Auditlog's log entries, for example in templates. - By default this field will assume that your primary keys are numeric, simply because this is the most + By default, this field will assume that your primary keys are numeric, simply because this is the most common case. However, if you have a non-integer primary key, you can simply pass ``pk_indexable=False`` to the constructor, and Auditlog will fall back to using a non-indexed text based field for this model. @@ -532,3 +534,24 @@ def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS): # method. However, because we don't want to delete these related # objects, we simply return an empty list. return [] + + +# should I add a signal receiver for setting_changed? +changes_func = None + + +def _changes_func() -> Callable[[LogEntry], Dict]: + def json_then_text(instance: LogEntry) -> 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: + return instance.changes or {} + + if settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT: + return json_then_text + return default diff --git a/auditlog_tests/test_two_step_json_migration.py b/auditlog_tests/test_two_step_json_migration.py new file mode 100644 index 00000000..8470cbf7 --- /dev/null +++ b/auditlog_tests/test_two_step_json_migration.py @@ -0,0 +1,151 @@ +import json +from io import StringIO +from unittest.mock import patch + +from django.core.management import call_command +from django.test import TestCase, override_settings + +from auditlog.models import LogEntry +from auditlog_tests.models import SimpleModel + + +class TwoStepMigrationTest(TestCase): + def test_use_text_changes_first(self): + text_obj = '{"field": "changes_text"}' + json_obj = {"field": "changes"} + _params = [ + (True, None, text_obj, {"field": "changes_text"}), + (True, json_obj, text_obj, json_obj), + (True, None, "not json", {}), + (False, json_obj, text_obj, json_obj), + ] + + for setting_value, changes_value, changes_text_value, expected in _params: + with self.subTest(): + entry = LogEntry(changes=changes_value, changes_text=changes_text_value) + with self.settings( + AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT=setting_value + ): + from auditlog import models + + changes_dict = models._changes_func()(entry) + self.assertEqual(changes_dict, expected) + + +class AuditlogMigrateJsonTest(TestCase): + def make_logentry(self): + model = SimpleModel.objects.create(text="I am a simple model.") + log_entry: LogEntry = model.history.first() + log_entry.changes_text = json.dumps(log_entry.changes) + log_entry.changes = None + log_entry.save() + return log_entry + + def call_command(self, *args, **kwargs): + outbuf = StringIO() + errbuf = StringIO() + call_command( + "auditlogmigratejson", *args, stdout=outbuf, stderr=errbuf, **kwargs + ) + return outbuf.getvalue().strip(), errbuf.getvalue().strip() + + def test_nothing_to_migrate(self): + outbuf, errbuf = self.call_command() + + msg = "All records are have been migrated." + self.assertEqual(outbuf, msg) + + @override_settings(AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT=True) + def test_nothing_to_migrate_with_conf_true(self): + outbuf, errbuf = self.call_command() + + msg = ( + "All records are have been migrated.\n" + "You can now set AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT to False." + ) + + self.assertEqual(outbuf, msg) + + def test_using_django(self): + # Arrange + log_entry = self.make_logentry() + + # Act + outbuf, errbuf = self.call_command("-b=0") + log_entry.refresh_from_db() + + # Assert + self.assertEqual(errbuf, "") + self.assertIsNotNone(log_entry.changes) + + def test_using_django_batched(self): + # Arrange + log_entry_1 = self.make_logentry() + log_entry_2 = self.make_logentry() + + # Act + outbuf, errbuf = self.call_command("-b=1") + log_entry_1.refresh_from_db() + log_entry_2.refresh_from_db() + + # Assert + self.assertEqual(errbuf, "") + self.assertIsNotNone(log_entry_1.changes) + self.assertIsNotNone(log_entry_2.changes) + + def test_using_django_batched_call_count(self): + """ + This is split into a different test because I couldn't figure out how to properly patch bulk_update. + For some reason, then I + """ + # Arrange + self.make_logentry() + self.make_logentry() + + # Act + with patch("auditlog.models.LogEntry.objects.bulk_update") as bulk_update: + outbuf, errbuf = self.call_command("-b=1") + call_count = bulk_update.call_count + + # Assert + self.assertEqual(call_count, 2) + + def test_native_postgres(self): + # Arrange + log_entry = self.make_logentry() + + # Act + outbuf, errbuf = self.call_command("-d=postgres") + log_entry.refresh_from_db() + + # Assert + self.assertEqual(errbuf, "") + self.assertIsNotNone(log_entry.changes) + + def test_native_unsupported(self): + # Arrange + log_entry = self.make_logentry() + + # Act + outbuf, errbuf = self.call_command("-d=oracle") + log_entry.refresh_from_db() + + # Assert + msg = "Not yet implemented. Run this management command without passing a -d/--database argument." + self.assertEqual(errbuf, msg) + self.assertIsNone(log_entry.changes) + + def test_using_django_with_error(self): + # Arrange + log_entry = self.make_logentry() + log_entry.changes_text = "not json" + log_entry.save() + + # Act + outbuf, errbuf = self.call_command() + log_entry.refresh_from_db() + + # Assert + msg = f"ValueError was raised while migrating the log with id {log_entry.id}." + self.assertEqual(errbuf, msg) + self.assertIsNone(log_entry.changes) diff --git a/docs/source/index.rst b/docs/source/index.rst index bf58a72d..d4f1a469 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -21,6 +21,7 @@ Contents installation usage + upgrade internals diff --git a/docs/source/upgrade.rst b/docs/source/upgrade.rst new file mode 100644 index 00000000..19410252 --- /dev/null +++ b/docs/source/upgrade.rst @@ -0,0 +1,21 @@ +Upgrading to version 3 +====================== + +Version 3.0.0 introduces breaking changes. Please review the migration guide below before upgrading. +If you're new to django-auditlog, you can ignore this part. + +The major change in the version is that we're finally storing changes as json instead of json-text. +To convert the existing records, this version has a database migration that does just that. +However, this migration will take a long time if you have a huge amount of records, +causing your database and application to be out of sync until the migration is complete. + +To avoid this, follow these steps: + +1. Before upgrading the package, add these two variables to ``settings.py``: + + * ``AUDITLOG_TWO_STEP_MIGRATION = True`` + * ``AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT = True`` + +2. Upgrade the package. Your app will now start storing new records as JSON, but the old records will accessible via ``LogEntry.changes_text``. +3. Use the newly added ``auditlogmigratejson`` command to migrate your records. Run ``django-admin auditlogmigratejson --help`` to get more information. +4. Once all records are migrated, remove the variables listed above, or set their values to ``False``. From 04fae7fe043b5c6af104e59be7cb73988a0579a5 Mon Sep 17 00:00:00 2001 From: James Gillard Date: Thu, 2 Nov 2023 07:24:28 +0000 Subject: [PATCH 089/126] Confirm Python 3.12 support (#572) * Confirm Python 3.12 support * Remove dj4.2/py3.12 from tox.ini --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 1 + setup.py | 1 + tox.ini | 4 +++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd773a1d..2fd452a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] services: postgres: diff --git a/CHANGELOG.md b/CHANGELOG.md index e22debfa..8914baad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### Breaking Changes +- Python: Confirm Python 3.12 support ([#572](https://github.com/jazzband/django-auditlog/pull/572)) - feat: Change `LogEntry.change` field type to `JSONField` rather than `TextField`. This change include a migration that may take time to run depending on the number of records on your `LogEntry` table ([#407](https://github.com/jazzband/django-auditlog/pull/407))([#495](https://github.com/jazzband/django-auditlog/pull/495)) - feat: stop deleting old log entries when a model with the same pk is created (i.e. the pk value is reused) ([#559](https://github.com/jazzband/django-auditlog/pull/559)) - feat: Set `AuditlogHistoryField.delete_related` to `False` by default. This is different from the default configuration of Django's `GenericRelation`, but we should not erase the audit log of objects on deletion by default. ([#557](https://github.com/jazzband/django-auditlog/pull/557)) diff --git a/setup.py b/setup.py index de2471ba..781e0cd4 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.1", diff --git a/tox.ini b/tox.ini index 98da3309..8b95ea51 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = {py38,py39,py310}-django32 {py38,py39,py310,py311}-django{41,42} - {py310,py311}-djangomain + {py310,py311,py312}-djangomain py38-docs py38-lint @@ -31,6 +31,7 @@ passenv= TEST_DB_PORT basepython = + py312: python3.12 py311: python3.11 py310: python3.10 py39: python3.9 @@ -52,3 +53,4 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 From 8aaf5a1d69c1fb396dbb6dae7b52738be7065dbb Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Wed, 13 Sep 2023 19:03:04 +0300 Subject: [PATCH 090/126] chore: add a check arg to the migration command (#563) * chore: add a check arg to the migration command * chore: add a check arg to the migration command --- .../commands/auditlogmigratejson.py | 64 +++++++++++++------ .../test_two_step_json_migration.py | 38 ++++++++--- 2 files changed, 74 insertions(+), 28 deletions(-) diff --git a/auditlog/management/commands/auditlogmigratejson.py b/auditlog/management/commands/auditlogmigratejson.py index db27ef6c..44871a81 100644 --- a/auditlog/management/commands/auditlogmigratejson.py +++ b/auditlog/management/commands/auditlogmigratejson.py @@ -1,6 +1,7 @@ from math import ceil from django.conf import settings +from django.core.management import CommandError, CommandParser from django.core.management.base import BaseCommand from auditlog.models import LogEntry @@ -8,21 +9,30 @@ class Command(BaseCommand): help = "Migrates changes from changes_text to json changes." - - def add_arguments(self, parser): - parser.add_argument( + requires_migrations_checks = True + + def add_arguments(self, parser: CommandParser): + group = parser.add_argument_group() + group.add_argument( + "--check", + action="store_true", + help="Just check the status of the migration", + dest="check", + ) + group.add_argument( "-d", "--database", default=None, + metavar="The database engine", help="If provided, the script will use native db operations. " - "Otherwise, it will use LogEntry.objects.bulk_create", + "Otherwise, it will use LogEntry.objects.bulk_update", dest="db", type=str, choices=["postgres", "mysql", "oracle"], ) - parser.add_argument( + group.add_argument( "-b", - "--bactch-size", + "--batch-size", default=500, help="Split the migration into multiple batches. If 0, then no batching will be done. " "When passing a -d/database, the batch value will be ignored.", @@ -33,18 +43,23 @@ def add_arguments(self, parser): def handle(self, *args, **options): database = options["db"] batch_size = options["batch_size"] + check = options["check"] - if not self.check_logs(): + if (not self.check_logs()) or check: return if database: result = self.migrate_using_sql(database) self.stdout.write( - f"Updated {result} records using native database operations." + self.style.SUCCESS( + f"Updated {result} records using native database operations." + ) ) else: result = self.migrate_using_django(batch_size) - self.stdout.write(f"Updated {result} records using django operations.") + self.stdout.write( + self.style.SUCCESS(f"Updated {result} records using django operations.") + ) self.check_logs() @@ -54,11 +69,12 @@ def check_logs(self): self.stdout.write(f"There are {count} records that needs migration.") return True - self.stdout.write("All records are have been migrated.") + self.stdout.write(self.style.SUCCESS("All records have been migrated.")) if settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT: - self.stdout.write( - "You can now set AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT to False." + var_msg = self.style.WARNING( + "AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT" ) + self.stdout.write(f"You can now set {var_msg} to False.") return False @@ -72,17 +88,25 @@ def _apply_django_migration(_logs) -> int: import json updated = [] + errors = [] for log in _logs: try: log.changes = json.loads(log.changes_text) except ValueError: - self.stderr.write( - f"ValueError was raised while migrating the log with id {log.id}." - ) + errors.append(log.id) else: updated.append(log) LogEntry.objects.bulk_update(updated, fields=["changes"]) + if errors: + self.stderr.write( + self.style.ERROR( + f"ValueError was raised while converting the logs with these ids into json." + f"They where not be included in this migration batch." + f"\n" + f"{errors}" + ) + ) return len(updated) logs = self.get_logs() @@ -107,8 +131,8 @@ def postgres(): if database == "postgres": return postgres() - else: - self.stderr.write( - "Not yet implemented. Run this management command without passing a -d/--database argument." - ) - return 0 + + raise CommandError( + f"Migrating the records using {database} is not implemented. " + f"Run this management command without passing a -d/--database argument." + ) diff --git a/auditlog_tests/test_two_step_json_migration.py b/auditlog_tests/test_two_step_json_migration.py index 8470cbf7..5beb560e 100644 --- a/auditlog_tests/test_two_step_json_migration.py +++ b/auditlog_tests/test_two_step_json_migration.py @@ -2,7 +2,7 @@ from io import StringIO from unittest.mock import patch -from django.core.management import call_command +from django.core.management import CommandError, call_command from django.test import TestCase, override_settings from auditlog.models import LogEntry @@ -52,7 +52,7 @@ def call_command(self, *args, **kwargs): def test_nothing_to_migrate(self): outbuf, errbuf = self.call_command() - msg = "All records are have been migrated." + msg = "All records have been migrated." self.assertEqual(outbuf, msg) @override_settings(AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT=True) @@ -60,12 +60,25 @@ def test_nothing_to_migrate_with_conf_true(self): outbuf, errbuf = self.call_command() msg = ( - "All records are have been migrated.\n" + "All records have been migrated.\n" "You can now set AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT to False." ) self.assertEqual(outbuf, msg) + def test_check(self): + # Arrange + log_entry = self.make_logentry() + + # Act + outbuf, errbuf = self.call_command("--check") + log_entry.refresh_from_db() + + # Assert + self.assertEqual("There are 1 records that needs migration.", outbuf) + self.assertEqual("", errbuf) + self.assertIsNone(log_entry.changes) + def test_using_django(self): # Arrange log_entry = self.make_logentry() @@ -125,14 +138,18 @@ def test_native_postgres(self): def test_native_unsupported(self): # Arrange log_entry = self.make_logentry() + msg = ( + "Migrating the records using oracle is not implemented. " + "Run this management command without passing a -d/--database argument." + ) # Act - outbuf, errbuf = self.call_command("-d=oracle") + with self.assertRaises(CommandError) as cm: + self.call_command("-d=oracle") log_entry.refresh_from_db() # Assert - msg = "Not yet implemented. Run this management command without passing a -d/--database argument." - self.assertEqual(errbuf, msg) + self.assertEqual(msg, cm.exception.args[0]) self.assertIsNone(log_entry.changes) def test_using_django_with_error(self): @@ -146,6 +163,11 @@ def test_using_django_with_error(self): log_entry.refresh_from_db() # Assert - msg = f"ValueError was raised while migrating the log with id {log_entry.id}." - self.assertEqual(errbuf, msg) + msg = ( + f"ValueError was raised while converting the logs with these ids into json." + f"They where not be included in this migration batch." + f"\n" + f"{[log_entry.id]}" + ) + self.assertEqual(msg, errbuf) self.assertIsNone(log_entry.changes) From d606aeb2e968249f30cae3282b0543e225329150 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:08:30 +0200 Subject: [PATCH 091/126] [pre-commit.ci] pre-commit autoupdate (#562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb253d2e..cb16820f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black language_version: python3.8 From 8802cbeb4b8ba71ef56c87e6045956622a93c23f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 22:33:26 +0200 Subject: [PATCH 092/126] [pre-commit.ci] pre-commit autoupdate (#564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.10.1 → v3.11.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.11.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb16820f..33d7e694 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + rev: v3.11.0 hooks: - id: pyupgrade args: [--py38-plus] From 1b8e78a10d30b6294419d34875931c082d1bbec6 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Wed, 20 Sep 2023 13:23:45 +0300 Subject: [PATCH 093/126] fix: don't set the correlation_id if the `AUDITLOG_CID_GETTER` is `None` (#565) --- CHANGELOG.md | 15 +++++++++++++-- auditlog/cid.py | 7 ++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8914baad..39bb7564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,22 @@ ## Next Release +#### Improvements +- Python: Confirm Python 3.12 support ([#572](https://github.com/jazzband/django-auditlog/pull/572)) + +## 3.0.0-beta.2 (2023-10-05) + +#### Breaking Changes +- feat: stop deleting old log entries when a model with the same pk is created (i.e. the pk value is reused) ([#559](https://github.com/jazzband/django-auditlog/pull/559)) + +#### Fixes +* fix: don't set the correlation_id if the `AUDITLOG_CID_GETTER` is `None` ([#565](https://github.com/jazzband/django-auditlog/pull/565)) + +## 3.0.0-beta.1 + #### Breaking Changes -- Python: Confirm Python 3.12 support ([#572](https://github.com/jazzband/django-auditlog/pull/572)) - feat: Change `LogEntry.change` field type to `JSONField` rather than `TextField`. This change include a migration that may take time to run depending on the number of records on your `LogEntry` table ([#407](https://github.com/jazzband/django-auditlog/pull/407))([#495](https://github.com/jazzband/django-auditlog/pull/495)) -- feat: stop deleting old log entries when a model with the same pk is created (i.e. the pk value is reused) ([#559](https://github.com/jazzband/django-auditlog/pull/559)) - feat: Set `AuditlogHistoryField.delete_related` to `False` by default. This is different from the default configuration of Django's `GenericRelation`, but we should not erase the audit log of objects on deletion by default. ([#557](https://github.com/jazzband/django-auditlog/pull/557)) - Python: Drop support for Python 3.7 ([#546](https://github.com/jazzband/django-auditlog/pull/546)) diff --git a/auditlog/cid.py b/auditlog/cid.py index e5308699..8d2aa9f8 100644 --- a/auditlog/cid.py +++ b/auditlog/cid.py @@ -13,11 +13,16 @@ def set_cid(request: Optional[HttpRequest] = 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`. - Note: we look for the header in `request.headers` and `request.META`. + Note: we look for the value of `AUDITLOG_CID_HEADER` in `request.headers` and in `request.META`. + + This function doesn't do anything if the user is supplying their own `AUDITLOG_CID_GETTER`. :param request: The request to get the cid from. :return: None """ + if settings.AUDITLOG_CID_GETTER: + return + cid = None header = settings.AUDITLOG_CID_HEADER From 284666c27829662cec5529e99c7ba8583d304514 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Tue, 26 Sep 2023 08:29:41 +0200 Subject: [PATCH 094/126] Update postgres to 13 (#568) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2fd452a2..98a922fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: services: postgres: - image: postgres:12 + image: postgres:13 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres From e7684fccf8b1d0622b6ac9f9f9789dce0281a7cf Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Tue, 26 Sep 2023 16:26:26 +0200 Subject: [PATCH 095/126] Update pre-commit repos (#569) --- .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 33d7e694..4346fefa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,12 +18,12 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.11.0 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.14.0 + rev: 1.15.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] From 757aa9908b14126b34c07379c7489a43e1711529 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Thu, 5 Oct 2023 12:52:45 +0300 Subject: [PATCH 096/126] fix: change verbose_name in changes_text migration (#571) --- auditlog/migrations/0017_alter_logentry_changes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auditlog/migrations/0017_alter_logentry_changes.py b/auditlog/migrations/0017_alter_logentry_changes.py index a2324f28..005e58e6 100644 --- a/auditlog/migrations/0017_alter_logentry_changes.py +++ b/auditlog/migrations/0017_alter_logentry_changes.py @@ -24,7 +24,7 @@ def two_step_migrations() -> List: migrations.AddField( model_name="logentry", name="changes_text", - field=models.TextField(blank=True, verbose_name="text change message"), + field=models.TextField(blank=True, verbose_name="change message"), ), migrations.AlterField( model_name="logentry", From 188891aa3fa0702141cfbc4bc6765734b2bea2e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:53:34 +0330 Subject: [PATCH 097/126] [pre-commit.ci] pre-commit autoupdate (#579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.0 → 23.10.1](https://github.com/psf/black/compare/23.10.0...23.10.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4346fefa..bf9b1cd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black language_version: python3.8 From 0550ea7dc936e4d27338979c95cab9fe947f1b61 Mon Sep 17 00:00:00 2001 From: hamsh Date: Mon, 6 Nov 2023 12:35:34 +0330 Subject: [PATCH 098/126] use contextvar instead of threadlocal (#581) * use contextvar instead of threadlocal * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update auditlog/context.py * update CHANGELOG.md --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hasan Ramezani --- CHANGELOG.md | 2 ++ auditlog/context.py | 38 +++++++++++++++++++++----------------- auditlog/receivers.py | 8 ++++++-- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39bb7564..e46b1de7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ ## Next Release #### Improvements + - Python: Confirm Python 3.12 support ([#572](https://github.com/jazzband/django-auditlog/pull/572)) +- feat: `thread.local` replaced with `ContextVar` to improve context managers in Django 4.2+ ## 3.0.0-beta.2 (2023-10-05) diff --git a/auditlog/context.py b/auditlog/context.py index 80d2fae5..644c6ce5 100644 --- a/auditlog/context.py +++ b/auditlog/context.py @@ -1,32 +1,33 @@ import contextlib -import threading import time +from contextvars import ContextVar from functools import partial +from django.contrib.auth import get_user_model from django.db.models.signals import pre_save from auditlog.models import LogEntry -threadlocal = threading.local() +auditlog_value = ContextVar("auditlog_value") +auditlog_disabled = ContextVar("auditlog_disabled", default=False) @contextlib.contextmanager def set_actor(actor, remote_addr=None): """Connect a signal receiver with current user attached.""" # Initialize thread local storage - threadlocal.auditlog = { + context_data = { "signal_duid": ("set_actor", time.time()), "remote_addr": remote_addr, } + auditlog_value.set(context_data) # Connect signal for automatic logging - set_actor = partial( - _set_actor, user=actor, signal_duid=threadlocal.auditlog["signal_duid"] - ) + set_actor = partial(_set_actor, user=actor, signal_duid=context_data["signal_duid"]) pre_save.connect( set_actor, sender=LogEntry, - dispatch_uid=threadlocal.auditlog["signal_duid"], + dispatch_uid=context_data["signal_duid"], weak=False, ) @@ -34,12 +35,11 @@ def set_actor(actor, remote_addr=None): yield finally: try: - auditlog = threadlocal.auditlog - except AttributeError: + auditlog = auditlog_value.get() + except LookupError: pass else: pre_save.disconnect(sender=LogEntry, dispatch_uid=auditlog["signal_duid"]) - del threadlocal.auditlog def _set_actor(user, sender, instance, signal_duid, **kwargs): @@ -48,14 +48,18 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs): This function becomes a valid signal receiver when it is curried with the actor and a dispatch id. """ try: - auditlog = threadlocal.auditlog - except AttributeError: + auditlog = auditlog_value.get() + except LookupError: pass else: if signal_duid != auditlog["signal_duid"]: return - - if sender == LogEntry and instance.actor is None: + auth_user_model = get_user_model() + if ( + sender == LogEntry + and isinstance(user, auth_user_model) + and instance.actor is None + ): instance.actor = user instance.remote_addr = auditlog["remote_addr"] @@ -63,11 +67,11 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs): @contextlib.contextmanager def disable_auditlog(): - threadlocal.auditlog_disabled = True + token = auditlog_disabled.set(True) try: yield finally: try: - del threadlocal.auditlog_disabled - except AttributeError: + auditlog_disabled.reset(token) + except LookupError: pass diff --git a/auditlog/receivers.py b/auditlog/receivers.py index c75619d7..53d0fb07 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -2,7 +2,7 @@ from django.conf import settings -from auditlog.context import threadlocal +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 @@ -17,7 +17,11 @@ def check_disable(signal_handler): @wraps(signal_handler) def wrapper(*args, **kwargs): - if not getattr(threadlocal, "auditlog_disabled", False) and not ( + try: + auditlog_disabled_value = auditlog_disabled.get() + except LookupError: + auditlog_disabled_value = False + if not auditlog_disabled_value and not ( kwargs.get("raw") and settings.AUDITLOG_DISABLE_ON_RAW_SAVE ): signal_handler(*args, **kwargs) From 60caa0a03672a19238f759a40808f3d5d2e591a7 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Tue, 14 Nov 2023 11:02:31 +0330 Subject: [PATCH 099/126] Prepare release 3.0.0-beta.3 (#587) --- .github/workflows/release.yml | 2 +- CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ca91dd7..823e65db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,4 +50,4 @@ jobs: with: user: jazzband password: ${{ secrets.JAZZBAND_RELEASE_KEY }} - repository_url: https://jazzband.co/projects/django-auditlog/upload + repository-url: https://jazzband.co/projects/django-auditlog/upload diff --git a/CHANGELOG.md b/CHANGELOG.md index e46b1de7..3854d5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next Release +## 3.0.0-beta.3 (2023-11-13) + #### Improvements - Python: Confirm Python 3.12 support ([#572](https://github.com/jazzband/django-auditlog/pull/572)) From 3054882388771a344d9702d9e316f77dc7332fbd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 22:48:12 +0330 Subject: [PATCH 100/126] [pre-commit.ci] pre-commit autoupdate (#594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/isort: 5.12.0 → 5.13.0](https://github.com/PyCQA/isort/compare/5.12.0...5.13.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf9b1cd6..50a1100a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: flake8 args: ["--max-line-length", "110"] - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.0 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade From a23195d2b0b1f955c21ba1c9333f79b92413b27b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:01:49 +0330 Subject: [PATCH 101/126] [pre-commit.ci] pre-commit autoupdate (#595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.11.0 → 23.12.0](https://github.com/psf/black/compare/23.11.0...23.12.0) - [github.com/PyCQA/isort: 5.13.0 → 5.13.2](https://github.com/PyCQA/isort/compare/5.13.0...5.13.2) 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 50a1100a..61480368 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.12.0 hooks: - id: black language_version: python3.8 @@ -14,7 +14,7 @@ repos: - id: flake8 args: ["--max-line-length", "110"] - repo: https://github.com/PyCQA/isort - rev: 5.13.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade From 119669518208a3bc10ac33b3b52642fbdbb416f8 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Fri, 29 Dec 2023 18:46:54 +0330 Subject: [PATCH 102/126] Confrim Django 5.0 and drop django 4.1 (#598) --- CHANGELOG.md | 5 +++++ docs/source/installation.rst | 4 ++-- setup.py | 2 +- tox.ini | 6 +++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3854d5ed..adb45929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Next Release +#### Improvements +- feat: If any receiver returns False, no logging will be made. This can be useful if logging should be conditionally enabled / disabled ([#590](https://github.com/jazzband/django-auditlog/pull/590)) +- Django: Confirm Django 5.0 support ([#598](https://github.com/jazzband/django-auditlog/pull/598)) +- Django: Drop Django 4.1 support ([#598](https://github.com/jazzband/django-auditlog/pull/598)) + ## 3.0.0-beta.3 (2023-11-13) #### Improvements diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 0df17b78..6a3a86d5 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -12,9 +12,9 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. **Requirements** - Python 3.8 or higher -- Django 3.2, 4.1 and 4.2 +- Django 3.2, 4.2 and 5.0 -Auditlog is currently tested with Python 3.8+ and Django 3.2, 4.1 and 4.2. The latest test report can be found +Auditlog is currently tested with Python 3.8+ and Django 3.2, 4.2 and 5.0. 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 781e0cd4..e2a63323 100644 --- a/setup.py +++ b/setup.py @@ -44,8 +44,8 @@ "Programming Language :: Python :: 3.12", "Framework :: Django", "Framework :: Django :: 3.2", - "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", "License :: OSI Approved :: MIT License", ], ) diff --git a/tox.ini b/tox.ini index 8b95ea51..820efd6b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = {py38,py39,py310}-django32 - {py38,py39,py310,py311}-django{41,42} - {py310,py311,py312}-djangomain + {py38,py39,py310,py311}-django42 + {py310,py311,py312}-django{50,main} py38-docs py38-lint @@ -14,8 +14,8 @@ commands = coverage xml deps = django32: Django>=3.2,<3.3 - django41: Django>=4.1,<4.2 django42: Django>=4.2,<4.3 + django50: Django>=5.0,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz # Test requirements coverage From 3daa5b572f3b29afe5b32a1b63f8b2814d5519dd Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 3 Jan 2024 11:29:07 +0330 Subject: [PATCH 103/126] Prepare release 3.0.0-beta.4 (#599) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index adb45929..d5f366f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next Release +## 3.0.0-beta.4 (2024-01-02) + #### Improvements - feat: If any receiver returns False, no logging will be made. This can be useful if logging should be conditionally enabled / disabled ([#590](https://github.com/jazzband/django-auditlog/pull/590)) - Django: Confirm Django 5.0 support ([#598](https://github.com/jazzband/django-auditlog/pull/598)) From 18eeb9ee1b1508751254b4d97a3c90798f6ac906 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 20:08:09 +0330 Subject: [PATCH 104/126] [pre-commit.ci] pre-commit autoupdate (#601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 61480368..e4d8b681 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - "--target-version" - "py38" - repo: https://github.com/PyCQA/flake8 - rev: "6.0.0" + rev: "7.0.0" hooks: - id: flake8 args: ["--max-line-length", "110"] From b6717922ae6ccaf4616beec9590b5fcaed8c1980 Mon Sep 17 00:00:00 2001 From: Pascal Mathis Date: Tue, 13 Feb 2024 20:05:27 +0100 Subject: [PATCH 105/126] fix: avoid exception in changes_display_dict when model is missing (#609) --- auditlog/models.py | 17 ++++++++++++----- auditlog_tests/tests.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/auditlog/models.py b/auditlog/models.py index 09086d0c..93848a0e 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -423,11 +423,14 @@ def changes_display_dict(self): """ :return: The changes recorded in this log entry intended for display to users as a dictionary object. """ - # Get the model and model_fields from auditlog.registry import auditlog + # Get the model and model_fields, but gracefully handle the case where the model no longer exists model = self.content_type.model_class() - model_fields = auditlog.get_model_fields(model._meta.model) + model_fields = None + if auditlog.contains(model._meta.model): + model_fields = auditlog.get_model_fields(model._meta.model) + changes_display_dict = {} # grab the changes_dict and iterate through for field_name, values in self.changes_dict.items(): @@ -485,9 +488,13 @@ def changes_display_dict(self): value = f"{value[:140]}..." values_display.append(value) - verbose_name = model_fields["mapping_fields"].get( - field.name, getattr(field, "verbose_name", field.name) - ) + + # Use verbose_name from mapping if available, otherwise determine from field + if model_fields and field.name in model_fields["mapping_fields"]: + verbose_name = model_fields["mapping_fields"][field.name] + else: + verbose_name = getattr(field, "verbose_name", field.name) + changes_display_dict[verbose_name] = values_display return changes_display_dict diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index d121f0d4..45cdd376 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -2379,3 +2379,19 @@ def test_m2m(self): self.assertEqual(0, LogEntry.objects.get_for_object(recursive).count()) related = ManyRelatedOtherModel.objects.get(pk=1) self.assertEqual(0, LogEntry.objects.get_for_object(related).count()) + + +class MissingModelTest(TestCase): + def setUp(self): + # Create a log entry, then unregister the model + self.obj = SimpleModel.objects.create(text="I am old.") + auditlog.unregister(SimpleModel) + + def tearDown(self): + # Re-register the model for other tests + auditlog.register(SimpleModel) + + def test_get_changes_for_missing_model(self): + history = self.obj.history.latest() + self.assertEqual(history.changes_dict["text"][1], self.obj.text) + self.assertEqual(history.changes_display_dict["text"][1], self.obj.text) From 3f5a07cd785f747a0440d56c593e15c9e2acabf6 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 20 Mar 2024 18:26:14 +0100 Subject: [PATCH 106/126] Keep GitHub Actions up to date with GitHub's Dependabot (#618) Fixes warnings like at the bottom right of https://github.com/jazzband/django-auditlog/actions/runs/8332793947 * https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot * https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..be006de9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly From 26971d1e06da783f8b188149483345e95a123f24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:11:40 +0100 Subject: [PATCH 107/126] Bump the github-actions group with 4 updates (#619) Bumps the github-actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python), [actions/cache](https://github.com/actions/cache) and [codecov/codecov-action](https://github.com/codecov/codecov-action). Updates `actions/checkout` from 3 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) Updates `actions/setup-python` from 3 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v5) Updates `actions/cache` from 3 to 4 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) Updates `codecov/codecov-action` from 3 to 4 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 6 +++--- .github/workflows/test.yml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 823e65db..c42ca268 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -26,7 +26,7 @@ jobs: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: release-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98a922fb..e97ef16a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,10 +27,10 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -40,7 +40,7 @@ jobs: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: @@ -64,6 +64,6 @@ jobs: TEST_DB_PORT: ${{ job.services.postgres.ports[5432] }} - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: name: Python ${{ matrix.python-version }} From 6165578e69c1c203e1104d8acd93a136b6c806ec Mon Sep 17 00:00:00 2001 From: Paul Morin <44358446+plmrn@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:20:41 +0100 Subject: [PATCH 108/126] Correcting a typo in usage.rst (#615) The code example for AUDITLOG_INCLUDE_TRACKING_MODELS made two distinct references to "model1" which seems to be a typo, should read "model2" on the second reference --- docs/source/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 376737a3..9e5ac990 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -233,7 +233,7 @@ It must be a list or tuple. Each item in this setting can be a: AUDITLOG_INCLUDE_TRACKING_MODELS = ( ".", { - "model": ".", + "model": ".", "include_fields": ["field1", "field2"], "exclude_fields": ["field3", "field4"], "mapping_fields": { From 49dc9bfe788bec7a2e6a08d2cf53ce6014faaf76 Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 31 Mar 2024 01:37:39 +0100 Subject: [PATCH 109/126] Disable logging remote IP address (#620) * Disable logging remote IP address * Update auditlog/middleware.py * Update CHANGELOG.md * Update auditlog/middleware.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update auditlog/middleware.py and add tests in ManyRelatedModelTest * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Hasan Ramezani Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + auditlog/conf.py | 5 +++++ auditlog/middleware.py | 6 ++++++ auditlog_tests/tests.py | 14 ++++++++++++++ docs/source/usage.rst | 13 +++++++++++++ 5 files changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f366f5..47e3c7c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## 3.0.0-beta.4 (2024-01-02) #### Improvements +- feat: Excluding ip address when `AUDITLOG_DISABLE_REMOTE_ADDR` is set to True ([#620](https://github.com/jazzband/django-auditlog/pull/620)) - feat: If any receiver returns False, no logging will be made. This can be useful if logging should be conditionally enabled / disabled ([#590](https://github.com/jazzband/django-auditlog/pull/590)) - Django: Confirm Django 5.0 support ([#598](https://github.com/jazzband/django-auditlog/pull/598)) - Django: Drop Django 4.1 support ([#598](https://github.com/jazzband/django-auditlog/pull/598)) diff --git a/auditlog/conf.py b/auditlog/conf.py index 9046669a..dbdfc5b4 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -40,3 +40,8 @@ settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT = getattr( settings, "AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT", False ) + +# Disable remote_addr field in database +settings.AUDITLOG_DISABLE_REMOTE_ADDR = getattr( + settings, "AUDITLOG_DISABLE_REMOTE_ADDR", False +) diff --git a/auditlog/middleware.py b/auditlog/middleware.py index e3274ee1..c47666bb 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth import get_user_model from auditlog.cid import set_cid @@ -12,9 +13,14 @@ class AuditlogMiddleware: def __init__(self, get_response=None): self.get_response = get_response + if not isinstance(settings.AUDITLOG_DISABLE_REMOTE_ADDR, bool): + raise TypeError("Setting 'AUDITLOG_DISABLE_REMOTE_ADDR' must be a boolean") @staticmethod def _get_remote_addr(request): + if settings.AUDITLOG_DISABLE_REMOTE_ADDR: + return None + # In case there is no proxy, return the original address if not request.headers.get("X-Forwarded-For"): return request.META.get("REMOTE_ADDR") diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 45cdd376..eb763d33 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -504,6 +504,20 @@ def test_exception(self): self.assert_no_listeners() + def test_init_middleware(self): + with override_settings(AUDITLOG_DISABLE_REMOTE_ADDR="str"): + with self.assertRaisesMessage( + TypeError, "Setting 'AUDITLOG_DISABLE_REMOTE_ADDR' must be a boolean" + ): + AuditlogMiddleware() + + def test_disable_remote_addr(self): + with override_settings(AUDITLOG_DISABLE_REMOTE_ADDR=True): + headers = {"HTTP_X_FORWARDED_FOR": "127.0.0.2"} + request = self.factory.get("/", **headers) + remote_addr = self.middleware._get_remote_addr(request) + self.assertIsNone(remote_addr) + def test_get_remote_addr(self): tests = [ # (headers, expected_remote_addr) ({}, "127.0.0.1"), diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 9e5ac990..8cabfbff 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -206,6 +206,19 @@ It will be considered when ``AUDITLOG_INCLUDE_ALL_MODELS`` is `True`. .. versionadded:: 3.0.0 +**AUDITLOG_EXCLUDE_TRACKING_FIELDS** + +When using "AuditlogMiddleware", +the IP address is logged by default, you can use this setting +to exclude the IP address from logging. +It will be considered when ``AUDITLOG_DISABLE_REMOTE_ADDR`` is `True`. + +.. code-block:: python + + AUDITLOG_DISABLE_REMOTE_ADDR = True + +.. versionadded:: 3.0.0 + **AUDITLOG_EXCLUDE_TRACKING_MODELS** You can use this setting to exclude models in registration process. From 4677a704693e730ac74e36f8fbff8ab173931fdc Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 7 Apr 2024 21:36:13 +0200 Subject: [PATCH 110/126] Add note about V3 to readme (#626) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6f0a94c3..d0f65c49 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ django-auditlog [![Supported Python versions](https://img.shields.io/pypi/pyversions/django-auditlog.svg)](https://pypi.python.org/pypi/django-auditlog) [![Supported Django versions](https://img.shields.io/pypi/djversions/django-auditlog.svg)](https://pypi.python.org/pypi/django-auditlog) +**Migrate to V3** + +Check the [Upgrading to version 3](https://django-auditlog.readthedocs.io/en/latest/upgrade.html) doc before upgrading to V3. + ```django-auditlog``` (Auditlog) is a reusable app for Django that makes logging object changes a breeze. Auditlog tries to use as much as Python and Django's built in functionality to keep the list of dependencies as short as possible. Also, Auditlog aims to be fast and simple to use. Auditlog is created out of the need for a simple Django app that logs changes to models along with the user who made the changes (later referred to as actor). Existing solutions seemed to offer a type of version control, which was found excessive and expensive in terms of database storage and performance. From 3c59b6f3c40044966c46e3d7198e998121e415d5 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Fri, 12 Apr 2024 10:14:42 +0200 Subject: [PATCH 111/126] Fixed manuall logging when model is not registered (#627) * Fixed manuall logging when model is not registered * Move change log --- CHANGELOG.md | 9 ++++++++- auditlog/models.py | 3 +++ auditlog_tests/tests.py | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e3c7c5..2f40e0ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ # Changes ## Next Release +#### Fixes -## 3.0.0-beta.4 (2024-01-02) +- Fixed manuall logging when model is not registered ([#627](https://github.com/jazzband/django-auditlog/pull/627)) #### Improvements - feat: Excluding ip address when `AUDITLOG_DISABLE_REMOTE_ADDR` is set to True ([#620](https://github.com/jazzband/django-auditlog/pull/620)) + + +## 3.0.0-beta.4 (2024-01-02) + +#### Improvements + - feat: If any receiver returns False, no logging will be made. This can be useful if logging should be conditionally enabled / disabled ([#590](https://github.com/jazzband/django-auditlog/pull/590)) - Django: Confirm Django 5.0 support ([#598](https://github.com/jazzband/django-auditlog/pull/598)) - Django: Drop Django 4.1 support ([#598](https://github.com/jazzband/django-auditlog/pull/598)) diff --git a/auditlog/models.py b/auditlog/models.py index 93848a0e..c34469ab 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -210,6 +210,9 @@ def _get_pk_value(self, instance): def _get_serialized_data_or_none(self, instance): from auditlog.registry import auditlog + if not auditlog.contains(instance.__class__): + return None + opts = auditlog.get_serialize_options(instance.__class__) if not opts["serialize_data"]: return None diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index eb763d33..6357db66 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1102,6 +1102,21 @@ def test_unregister_delete(self): # Check for log entries self.assertEqual(LogEntry.objects.count(), 0, msg="There are no log entries") + def test_manual_logging(self): + obj = self.obj + obj.boolean = True + obj.save() + LogEntry.objects.log_create( + instance=obj, + action=LogEntry.Action.UPDATE, + changes="", + ) + self.assertEqual( + obj.history.filter(action=LogEntry.Action.UPDATE).count(), + 1, + msg="There is one log entry for 'UPDATE'", + ) + class RegisterModelSettingsTest(TestCase): def setUp(self): From 64ec0c96ad729b82b724eddf8ad4d9780371236a Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Fri, 12 Apr 2024 11:51:31 +0200 Subject: [PATCH 112/126] Prepare release 3.0.0 (#629) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f40e0ec..d0d4c66a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changes ## Next Release + +## 3.0.0 (2024-04-12) + #### Fixes - Fixed manuall logging when model is not registered ([#627](https://github.com/jazzband/django-auditlog/pull/627)) From bec1c2da908384b2c746a46a16eeea5c27a2109e Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 29 Apr 2024 23:38:09 +0200 Subject: [PATCH 113/126] Fixed problem when setting `django.db.models.functions.Now()` in `DateTimeField` (#635) --- CHANGELOG.md | 4 ++++ auditlog/diff.py | 6 +++++- auditlog_tests/tests.py | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d4c66a..07427ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Release +#### Fixes + +- Fixed problem when setting `django.db.models.functions.Now()` in `DateTimeField` ([#635](https://github.com/jazzband/django-auditlog/pull/635)) + ## 3.0.0 (2024-04-12) #### Fixes diff --git a/auditlog/diff.py b/auditlog/diff.py index 7f4daae1..61f26cff 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -66,7 +66,11 @@ def get_field_value(obj, field): if isinstance(field, DateTimeField): # DateTimeFields are timezone-aware, so we need to convert the field # to its naive form before we can accurately compare them for changes. - value = field.to_python(getattr(obj, field.name, None)) + value = getattr(obj, field.name, None) + try: + value = field.to_python(value) + except TypeError: + return value if ( value is not None and settings.USE_TZ diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 6357db66..3e4209a1 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -17,6 +17,7 @@ from django.contrib.contenttypes.models import ContentType from django.core import management from django.db import models +from django.db.models.functions import Now from django.db.models.signals import pre_save from django.test import RequestFactory, TestCase, override_settings from django.urls import resolve, reverse @@ -1061,6 +1062,24 @@ def test_update_naive_dt(self): ) dtm.save() + def test_datetime_field_functions_now(self): + timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) + date = datetime.date(2017, 1, 10) + time = datetime.time(12, 0) + + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=Now(), + ) + dtm.save() + dtm.naive_dt = Now() + self.assertEqual(dtm.naive_dt, Now()) + dtm.save() + self.assertEqual(dtm.naive_dt, Now()) + class UnregisterTest(TestCase): def setUp(self): From efd0b7c5b7f7d3d9c9c7af7cc03703788cef9682 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 27 May 2024 13:22:46 +0330 Subject: [PATCH 114/126] Update postgres to 14 (#649) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e97ef16a..d024814b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: services: postgres: - image: postgres:13 + image: postgres:14 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres From ec43e0009d1b26150d22af9093a450f353fad5cd Mon Sep 17 00:00:00 2001 From: Mohsin Raza Date: Thu, 1 Aug 2024 13:33:40 +0500 Subject: [PATCH 115/126] update docs (#662) --- docs/source/usage.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 8cabfbff..843c189d 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -55,6 +55,22 @@ A DetailView utilizing the LogAccessMixin could look like the following example: # View code goes here +You can also add log-access to function base views, as the following example illustrates: + +.. code-block:: python + + from auditlog.signals import accessed + + def profile_view(request, pk): + ## get the object you want to log access + user = User.objects.get(pk=pk) + + ## log access + accessed.send(user.__class__, instance=user) + + # View code goes here + ... + **Excluding fields** From 40523befe0b2996a393b47b31f33622a84e23e4f Mon Sep 17 00:00:00 2001 From: Cleiton de Lima Date: Tue, 1 Oct 2024 13:19:58 -0300 Subject: [PATCH 116/126] Added --no-color args in test migration (#670) --- auditlog_tests/test_two_step_json_migration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/auditlog_tests/test_two_step_json_migration.py b/auditlog_tests/test_two_step_json_migration.py index 5beb560e..8e05fd9a 100644 --- a/auditlog_tests/test_two_step_json_migration.py +++ b/auditlog_tests/test_two_step_json_migration.py @@ -44,6 +44,7 @@ def make_logentry(self): def call_command(self, *args, **kwargs): outbuf = StringIO() errbuf = StringIO() + args = ("--no-color",) + args call_command( "auditlogmigratejson", *args, stdout=outbuf, stderr=errbuf, **kwargs ) From 242a333f6d4a4b2ca0786607ae552d46bba4eedb Mon Sep 17 00:00:00 2001 From: Cleiton de Lima Date: Mon, 7 Oct 2024 10:52:34 -0300 Subject: [PATCH 117/126] Added remote port (#671) --- .gitignore | 1 + CHANGELOG.md | 3 +++ auditlog/context.py | 4 +++- auditlog/middleware.py | 16 +++++++++++++++- .../migrations/0018_logentry_remote_port.py | 17 +++++++++++++++++ auditlog/models.py | 3 +++ auditlog_tests/tests.py | 15 ++++++++++++++- 7 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 auditlog/migrations/0018_logentry_remote_port.py diff --git a/.gitignore b/.gitignore index 155da2b3..96288a16 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ venv.bak/ ### JetBrains .idea/ +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 07427ee0..ad6760e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Next Release +#### Improvements +- feat: Added `LogEntry.remote_port` field. ([#671](https://github.com/jazzband/django-auditlog/pull/671)) + #### Fixes - Fixed problem when setting `django.db.models.functions.Now()` in `DateTimeField` ([#635](https://github.com/jazzband/django-auditlog/pull/635)) diff --git a/auditlog/context.py b/auditlog/context.py index 644c6ce5..ecb034c7 100644 --- a/auditlog/context.py +++ b/auditlog/context.py @@ -13,12 +13,13 @@ @contextlib.contextmanager -def set_actor(actor, remote_addr=None): +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()), "remote_addr": remote_addr, + "remote_port": remote_port, } auditlog_value.set(context_data) @@ -63,6 +64,7 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs): instance.actor = user instance.remote_addr = auditlog["remote_addr"] + instance.remote_port = auditlog["remote_port"] @contextlib.contextmanager diff --git a/auditlog/middleware.py b/auditlog/middleware.py index c47666bb..bd01da39 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,3 +1,5 @@ +from typing import Optional + from django.conf import settings from django.contrib.auth import get_user_model @@ -36,6 +38,17 @@ def _get_remote_addr(request): return remote_addr + @staticmethod + def _get_remote_port(request) -> Optional[int]: + remote_port = request.headers.get("X-Forwarded-Port", "") + + try: + remote_port = int(remote_port) + except ValueError: + remote_port = None + + return remote_port + @staticmethod def _get_actor(request): user = getattr(request, "user", None) @@ -45,9 +58,10 @@ def _get_actor(request): def __call__(self, request): remote_addr = self._get_remote_addr(request) + remote_port = self._get_remote_port(request) user = self._get_actor(request) set_cid(request) - with set_actor(actor=user, remote_addr=remote_addr): + with set_actor(actor=user, remote_addr=remote_addr, remote_port=remote_port): return self.get_response(request) diff --git a/auditlog/migrations/0018_logentry_remote_port.py b/auditlog/migrations/0018_logentry_remote_port.py new file mode 100644 index 00000000..0bfeaef9 --- /dev/null +++ b/auditlog/migrations/0018_logentry_remote_port.py @@ -0,0 +1,17 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("auditlog", "0017_alter_logentry_changes"), + ] + + operations = [ + migrations.AddField( + model_name="logentry", + name="remote_port", + field=models.PositiveIntegerField( + blank=True, null=True, verbose_name="remote port" + ), + ), + ] diff --git a/auditlog/models.py b/auditlog/models.py index c34469ab..66eee71e 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -358,6 +358,9 @@ class Action: remote_addr = models.GenericIPAddressField( blank=True, null=True, verbose_name=_("remote address") ) + remote_port = models.PositiveIntegerField( + blank=True, null=True, verbose_name=_("remote port") + ) timestamp = models.DateTimeField( default=django_timezone.now, db_index=True, diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 3e4209a1..228ddd76 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -537,6 +537,13 @@ def test_get_remote_addr(self): self.middleware._get_remote_addr(request), expected_remote_addr ) + def test_get_remote_port(self): + headers = { + "HTTP_X_FORWARDED_PORT": "12345", + } + request = self.factory.get("/", **headers) + self.assertEqual(self.middleware._get_remote_port(request), 12345) + def test_cid(self): header = str(settings.AUDITLOG_CID_HEADER).lstrip("HTTP_").replace("_", "-") header_meta = "HTTP_" + header.upper().replace("-", "_") @@ -574,9 +581,10 @@ def test_set_actor_anonymous_request(self): The remote address will be set even when there is no actor """ remote_addr = "123.213.145.99" + remote_port = 12345 actor = None - with set_actor(actor=actor, remote_addr=remote_addr): + with set_actor(actor=actor, remote_addr=remote_addr, remote_port=remote_port): obj = SimpleModel.objects.create(text="I am not difficult.") history = obj.history.get() @@ -585,6 +593,11 @@ def test_set_actor_anonymous_request(self): remote_addr, msg=f"Remote address is {remote_addr}", ) + self.assertEqual( + history.remote_port, + remote_port, + msg=f"Remote port is {remote_port}", + ) self.assertIsNone(history.actor, msg="Actor is `None` for anonymous user") def test_get_actor(self): From 8c76642064e2bbcc7f7f5c486a54a67ff2eda3d6 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 17 Oct 2024 18:40:21 +0200 Subject: [PATCH 118/126] Drop Python 3.8 support (#678) --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 6 ++-- CHANGELOG.md | 1 + .../migrations/0017_alter_logentry_changes.py | 4 +-- auditlog/models.py | 16 ++++----- auditlog/registry.py | 35 +++++++------------ docs/source/installation.rst | 4 +-- pyproject.toml | 2 +- setup.py | 9 ++--- tox.ini | 14 ++++---- 11 files changed, 39 insertions(+), 56 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c42ca268..e0935e22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: '3.9' - name: Get pip cache dir id: pip-cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d024814b..b28e2c4b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12'] services: postgres: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4d8b681..5613ca8f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,10 +4,10 @@ repos: rev: 23.12.0 hooks: - id: black - language_version: python3.8 + language_version: python3.9 args: - "--target-version" - - "py38" + - "py39" - repo: https://github.com/PyCQA/flake8 rev: "7.0.0" hooks: @@ -21,7 +21,7 @@ repos: rev: v3.13.0 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] - repo: https://github.com/adamchainz/django-upgrade rev: 1.15.0 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6760e1..f432c85b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### Improvements - feat: Added `LogEntry.remote_port` field. ([#671](https://github.com/jazzband/django-auditlog/pull/671)) +- Drop Python 3.8 support. ([#678](https://github.com/jazzband/django-auditlog/pull/678)) #### Fixes diff --git a/auditlog/migrations/0017_alter_logentry_changes.py b/auditlog/migrations/0017_alter_logentry_changes.py index 005e58e6..78f7447e 100644 --- a/auditlog/migrations/0017_alter_logentry_changes.py +++ b/auditlog/migrations/0017_alter_logentry_changes.py @@ -1,11 +1,9 @@ # Generated by Django 4.0 on 2022-08-04 15:41 -from typing import List - from django.conf import settings from django.db import migrations, models -def two_step_migrations() -> List: +def two_step_migrations() -> list: if settings.AUDITLOG_TWO_STEP_MIGRATION: return [ migrations.RenameField( diff --git a/auditlog/models.py b/auditlog/models.py index 66eee71e..23dbc689 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -3,7 +3,7 @@ import json from copy import deepcopy from datetime import timezone -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Union from dateutil import parser from dateutil.tz import gettz @@ -260,8 +260,8 @@ def _get_copy_with_python_typed_fields(self, instance): return instance_copy def _get_applicable_model_fields( - self, instance, model_fields: Dict[str, List[str]] - ) -> List[str]: + self, instance, model_fields: dict[str, list[str]] + ) -> list[str]: include_fields = model_fields["include_fields"] exclude_fields = model_fields["exclude_fields"] all_field_names = [field.name for field in instance._meta.fields] @@ -272,8 +272,8 @@ def _get_applicable_model_fields( return list(set(include_fields or all_field_names).difference(exclude_fields)) def _mask_serialized_fields( - self, data: Dict[str, Any], mask_fields: List[str] - ) -> Dict[str, Any]: + self, data: dict[str, Any], mask_fields: list[str] + ) -> dict[str, Any]: all_field_data = data.pop("fields") masked_field_data = {} @@ -553,8 +553,8 @@ 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[[LogEntry], dict]: + def json_then_text(instance: LogEntry) -> dict: if instance.changes: return instance.changes elif instance.changes_text: @@ -562,7 +562,7 @@ def json_then_text(instance: LogEntry) -> Dict: return json.loads(instance.changes_text) return {} - def default(instance: LogEntry) -> Dict: + def default(instance: LogEntry) -> dict: return instance.changes or {} if settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT: diff --git a/auditlog/registry.py b/auditlog/registry.py index 7dfb15f7..da299793 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -1,16 +1,7 @@ import copy from collections import defaultdict -from typing import ( - Any, - Callable, - Collection, - Dict, - Iterable, - List, - Optional, - Tuple, - Union, -) +from collections.abc import Collection, Iterable +from typing import Any, Callable, Optional, Union from django.apps import apps from django.db.models import ManyToManyField, Model @@ -26,7 +17,7 @@ from auditlog.conf import settings from auditlog.signals import accessed -DispatchUID = Tuple[int, int, int] +DispatchUID = tuple[int, int, int] class AuditLogRegistrationError(Exception): @@ -47,7 +38,7 @@ def __init__( delete: bool = True, access: bool = True, m2m: bool = True, - custom: Optional[Dict[ModelSignal, Callable]] = None, + custom: Optional[dict[ModelSignal, Callable]] = None, ): from auditlog.receivers import log_access, log_create, log_delete, log_update @@ -71,13 +62,13 @@ def __init__( 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, + 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, m2m_fields: Optional[Collection[str]] = None, serialize_data: bool = False, - serialize_kwargs: Optional[Dict[str, Any]] = None, + serialize_kwargs: Optional[dict[str, Any]] = None, serialize_auditlog_fields_only: bool = False, ): """ @@ -169,7 +160,7 @@ def unregister(self, model: ModelBase) -> None: else: self._disconnect_signals(model) - def get_models(self) -> List[ModelBase]: + def get_models(self) -> list[ModelBase]: """Get a list of all registered models.""" return list(self._registry.keys()) @@ -236,7 +227,7 @@ def _dispatch_uid(self, signal, receiver) -> DispatchUID: """Generate a dispatch_uid which is unique for a combination of self, signal, and receiver.""" return id(self), id(signal), id(receiver) - def _get_model_classes(self, app_model: str) -> List[ModelBase]: + def _get_model_classes(self, app_model: str) -> list[ModelBase]: try: try: app_label, model_name = app_model.split(".") @@ -248,7 +239,7 @@ def _get_model_classes(self, app_model: str) -> List[ModelBase]: def _get_exclude_models( self, exclude_tracking_models: Iterable[str] - ) -> List[ModelBase]: + ) -> list[ModelBase]: exclude_models = [ model for app_model in tuple(exclude_tracking_models) @@ -257,7 +248,7 @@ def _get_exclude_models( ] return exclude_models - def _register_models(self, models: Iterable[Union[str, Dict[str, Any]]]) -> None: + def _register_models(self, models: Iterable[Union[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 6a3a86d5..1a17eda2 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.8 or higher +- Python 3.9 or higher - Django 3.2, 4.2 and 5.0 -Auditlog is currently tested with Python 3.8+ and Django 3.2, 4.2 and 5.0. The latest test report can be found +Auditlog is currently tested with Python 3.9+ and Django 3.2, 4.2 and 5.0. The latest test report can be found at https://github.com/jazzband/django-auditlog/actions. Adding Auditlog to your Django application diff --git a/pyproject.toml b/pyproject.toml index 316468f3..e3c9789b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -target-version = ["py38"] +target-version = ["py39"] [tool.isort] profile = "black" diff --git a/setup.py b/setup.py index e2a63323..db0a8cac 100644 --- a/setup.py +++ b/setup.py @@ -28,16 +28,11 @@ description="Audit log app for Django", long_description=long_description, long_description_content_type="text/markdown", - python_requires=">=3.8", - install_requires=[ - "Django>=3.2", - "django-admin-rangefilter>=0.8.0", - "python-dateutil>=2.7.0", - ], + python_requires=">=3.9", + install_requires=["Django>=3.2", "python-dateutil>=2.7.0"], zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/tox.ini b/tox.ini index 820efd6b..3bf56322 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] envlist = - {py38,py39,py310}-django32 - {py38,py39,py310,py311}-django42 + {py39,py310}-django32 + {py39,py310,py311}-django42 {py310,py311,py312}-django{50,main} - py38-docs - py38-lint + py39-docs + py39-lint [testenv] setenv = @@ -35,21 +35,19 @@ basepython = py311: python3.11 py310: python3.10 py39: python3.9 - py38: python3.8 -[testenv:py38-docs] +[testenv:py39-docs] changedir = docs/source deps = -rdocs/requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html -[testenv:py38-lint] +[testenv:py39-lint] deps = pre-commit commands = pre-commit run --all-files [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 From d9a7c6a2e0c9716a57b5042a4a3c0a9bfd9c9fd4 Mon Sep 17 00:00:00 2001 From: Ilya Date: Tue, 21 Oct 2025 18:22:58 +0200 Subject: [PATCH 119/126] Replace index_together with indexes index_together is deprecated in Django 4.2. --- auditlog/migrations/0019_rename_logentry_idx.py | 15 +++++++++++++++ auditlog/models.py | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 auditlog/migrations/0019_rename_logentry_idx.py diff --git a/auditlog/migrations/0019_rename_logentry_idx.py b/auditlog/migrations/0019_rename_logentry_idx.py new file mode 100644 index 00000000..dbad3a95 --- /dev/null +++ b/auditlog/migrations/0019_rename_logentry_idx.py @@ -0,0 +1,15 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("auditlog", "0018_logentry_remote_port"), + ] + + operations = [ + migrations.RenameIndex( + model_name="logentry", + new_name="auditlog_timestamp_id_idx", + old_fields=("timestamp", "id"), + ), + ] diff --git a/auditlog/models.py b/auditlog/models.py index 23dbc689..26e96154 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -377,7 +377,9 @@ class Meta: ordering = ["-timestamp"] verbose_name = _("log entry") verbose_name_plural = _("log entries") - index_together = ("timestamp", "id") + indexes = [ + models.Index(fields=["timestamp", "id"], name="auditlog_timestamp_id_idx"), + ] def __str__(self): if self.action == self.Action.CREATE: From f6fb74dbf7fe8ef616c2eac9af5ae45a1a2ceaca Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 17 Oct 2024 18:53:55 +0200 Subject: [PATCH 120/126] Confirm Django 5.1 support and drop Django 3.2 support. (#677) --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 2 ++ docs/requirements.txt | 2 +- docs/source/installation.rst | 4 ++-- setup.py | 4 ++-- tox.ini | 5 ++--- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5613ca8f..5c42d46d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,4 +26,4 @@ repos: rev: 1.15.0 hooks: - id: django-upgrade - args: [--target-version, "3.2"] + args: [--target-version, "4.2"] diff --git a/CHANGELOG.md b/CHANGELOG.md index f432c85b..e3a81afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ ## Next Release #### Improvements + - feat: Added `LogEntry.remote_port` field. ([#671](https://github.com/jazzband/django-auditlog/pull/671)) - Drop Python 3.8 support. ([#678](https://github.com/jazzband/django-auditlog/pull/678)) +- Confirm Django 5.1 support and drop Django 3.2 support. ([#677](https://github.com/jazzband/django-auditlog/pull/677)) #### Fixes diff --git a/docs/requirements.txt b/docs/requirements.txt index 597a7cfd..eb4b4b66 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ # Docs requirements -django>=3.2,<3.3 +django>=4.2,<4.3 sphinx sphinx_rtd_theme psycopg2-binary diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 1a17eda2..b6b197a1 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -12,9 +12,9 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. **Requirements** - Python 3.9 or higher -- Django 3.2, 4.2 and 5.0 +- Django 4.2, 5.0 and 5.1 -Auditlog is currently tested with Python 3.9+ and Django 3.2, 4.2 and 5.0. The latest test report can be found +Auditlog is currently tested with Python 3.9+ and Django 4.2, 5.0 and 5.1. 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 db0a8cac..0bfbc3b7 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ long_description=long_description, long_description_content_type="text/markdown", python_requires=">=3.9", - install_requires=["Django>=3.2", "python-dateutil>=2.7.0"], + install_requires=["Django>=4.2", "python-dateutil>=2.7.0"], zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", @@ -38,9 +38,9 @@ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Framework :: Django", - "Framework :: Django :: 3.2", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", "License :: OSI Approved :: MIT License", ], ) diff --git a/tox.ini b/tox.ini index 3bf56322..c7e845a8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,7 @@ [tox] envlist = - {py39,py310}-django32 {py39,py310,py311}-django42 - {py310,py311,py312}-django{50,main} + {py310,py311,py312}-django{50,51,main} py39-docs py39-lint @@ -13,9 +12,9 @@ commands = coverage run --source auditlog runtests.py coverage xml deps = - django32: Django>=3.2,<3.3 django42: Django>=4.2,<4.3 django50: Django>=5.0,<5.1 + django51: Django>=5.1,<5.2 djangomain: https://github.com/django/django/archive/main.tar.gz # Test requirements coverage From f5485105d70e17f6f2f4841a2afd221719a4787e Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 17 Oct 2024 19:22:57 +0200 Subject: [PATCH 121/126] Update pre-commit (#679) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c42d46d..f80e2b17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 23.12.0 + rev: 24.10.0 hooks: - id: black language_version: python3.9 @@ -9,7 +9,7 @@ repos: - "--target-version" - "py39" - repo: https://github.com/PyCQA/flake8 - rev: "7.0.0" + rev: "7.1.1" hooks: - id: flake8 args: ["--max-line-length", "110"] @@ -18,12 +18,12 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.18.0 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.15.0 + rev: 1.22.1 hooks: - id: django-upgrade args: [--target-version, "4.2"] From 42b913c3804746bee76e06c80cafbfe9a5db32af Mon Sep 17 00:00:00 2001 From: Bahram Aghaei Date: Wed, 12 Jun 2024 14:30:29 +0200 Subject: [PATCH 122/126] Sync django query and postgres query (#653) * run postgres query for rows that changes is null for them and there is value for changes_text * add a test case to make when changes has value it wont be overwritten by changes_text --- .../management/commands/auditlogmigratejson.py | 8 +++++++- auditlog_tests/test_two_step_json_migration.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/auditlog/management/commands/auditlogmigratejson.py b/auditlog/management/commands/auditlogmigratejson.py index 44871a81..86caf25b 100644 --- a/auditlog/management/commands/auditlogmigratejson.py +++ b/auditlog/management/commands/auditlogmigratejson.py @@ -125,7 +125,13 @@ def migrate_using_sql(self, database): def postgres(): with connection.cursor() as cursor: cursor.execute( - 'UPDATE auditlog_logentry SET changes="changes_text"::jsonb' + """ + UPDATE auditlog_logentry + SET changes="changes_text"::jsonb + WHERE changes_text IS NOT NULL + AND changes_text <> '' + AND changes IS NULL + """ ) return cursor.cursor.rowcount diff --git a/auditlog_tests/test_two_step_json_migration.py b/auditlog_tests/test_two_step_json_migration.py index 8e05fd9a..7df115e8 100644 --- a/auditlog_tests/test_two_step_json_migration.py +++ b/auditlog_tests/test_two_step_json_migration.py @@ -136,6 +136,21 @@ def test_native_postgres(self): self.assertEqual(errbuf, "") self.assertIsNotNone(log_entry.changes) + def test_native_postgres_changes_not_overwritten(self): + # Arrange + log_entry = self.make_logentry() + log_entry.changes = original_changes = {"key": "value"} + log_entry.changes_text = '{"key": "new value"}' + log_entry.save() + + # Act + outbuf, errbuf = self.call_command("-d=postgres") + log_entry.refresh_from_db() + + # Assert + self.assertEqual(errbuf, "") + self.assertEqual(log_entry.changes, original_changes) + def test_native_unsupported(self): # Arrange log_entry = self.make_logentry() From 11d5ae68d1f79e224616e6da93eac0c9f88df690 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 29 Jan 2025 11:34:47 +0330 Subject: [PATCH 123/126] Add Python 3.13 support (#697) --- .github/workflows/test.yml | 8 ++++---- .pre-commit-config.yaml | 6 +++--- CHANGELOG.md | 1 + auditlog/models.py | 2 +- auditlog_tests/test_settings.py | 2 ++ setup.py | 1 + tox.ini | 6 +++++- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b28e2c4b..28f2888b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,22 +9,22 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] services: postgres: - image: postgres:14 + image: postgres:15 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - - 5432/tcp + - 5432/tcp options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s - --health-retries 5 + --health-retries 10 steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f80e2b17..fbf18742 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - "--target-version" - "py39" - repo: https://github.com/PyCQA/flake8 - rev: "7.1.1" + rev: "7.3.0" hooks: - id: flake8 args: ["--max-line-length", "110"] @@ -18,12 +18,12 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.18.0 + rev: v3.20.0 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.22.1 + rev: 1.25.0 hooks: - id: django-upgrade args: [--target-version, "4.2"] diff --git a/CHANGELOG.md b/CHANGELOG.md index e3a81afe..bc273968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### Improvements +- Add Python 3.13 support. ([#671](https://github.com/jazzband/django-auditlog/pull/671)) - feat: Added `LogEntry.remote_port` field. ([#671](https://github.com/jazzband/django-auditlog/pull/671)) - Drop Python 3.8 support. ([#678](https://github.com/jazzband/django-auditlog/pull/678)) - Confirm Django 5.1 support and drop Django 3.2 support. ([#677](https://github.com/jazzband/django-auditlog/pull/677)) diff --git a/auditlog/models.py b/auditlog/models.py index 26e96154..614ed24c 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -3,7 +3,7 @@ import json from copy import deepcopy from datetime import timezone -from typing import Any, Callable, Union +from typing import Any, Callable from dateutil import parser from dateutil.tz import gettz diff --git a/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py index 6eda1cb8..e89d659e 100644 --- a/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -1,6 +1,7 @@ """ Settings file for the Auditlog test suite. """ + import os DEBUG = True @@ -14,6 +15,7 @@ "django.contrib.sessions", "django.contrib.admin", "django.contrib.staticfiles", + "django.contrib.postgres", "auditlog", "auditlog_tests", ] diff --git a/setup.py b/setup.py index 0bfbc3b7..73da6621 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", diff --git a/tox.ini b/tox.ini index c7e845a8..dae56650 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,9 @@ [tox] envlist = {py39,py310,py311}-django42 - {py310,py311,py312}-django{50,51,main} + {py310,py311,py312}-django50 + {py310,py311,py312,py313}-django51 + {py312,py313}-djangomain py39-docs py39-lint @@ -30,6 +32,7 @@ passenv= TEST_DB_PORT basepython = + py313: python3.13 py312: python3.12 py311: python3.11 py310: python3.10 @@ -51,3 +54,4 @@ python = 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 From fc6bdc67d753e240e3f7558245a61391acf9b2d5 Mon Sep 17 00:00:00 2001 From: Youngkwang Yang Date: Wed, 24 Sep 2025 16:05:11 +0900 Subject: [PATCH 124/126] Fix Expression test compatibility for Django 6.0+ (#759) * Skip incompatible tests on Django 6.0+ refs: - #635 - #646 - https://code.djangoproject.com/ticket/27222 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove skipif * Add changelog --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- auditlog_tests/tests.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 228ddd76..c96857ad 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -9,6 +9,7 @@ import freezegun from dateutil.tz import gettz +from django import VERSION as DJANGO_VERSION from django.apps import apps from django.conf import settings from django.contrib.admin.sites import AdminSite @@ -1091,7 +1092,14 @@ def test_datetime_field_functions_now(self): dtm.naive_dt = Now() self.assertEqual(dtm.naive_dt, Now()) dtm.save() - self.assertEqual(dtm.naive_dt, Now()) + + # Django 6.0+ evaluates expressions during save (django ticket #27222) + if DJANGO_VERSION >= (6, 0, 0): + with self.subTest("After save Django 6.0+"): + self.assertIsInstance(dtm.naive_dt, datetime.datetime) + else: + with self.subTest("After save Django < 6.0"): + self.assertEqual(dtm.naive_dt, Now()) class UnregisterTest(TestCase): From 82ad79ac88e3dc92047b54c3be3ebd1f56a55fd7 Mon Sep 17 00:00:00 2001 From: Ilya Date: Tue, 21 Oct 2025 19:27:44 +0200 Subject: [PATCH 125/126] don't run tests on django main version, update fork maintainer --- setup.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 73da6621..ea7f6ca7 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ }, license="MIT", author="Jan-Jelle Kester", - maintainer="Alieh Rymašeŭski", + maintainer="Ilya Datskevich", description="Audit log app for Django", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tox.ini b/tox.ini index dae56650..a6ba9217 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = {py39,py310,py311}-django42 {py310,py311,py312}-django50 {py310,py311,py312,py313}-django51 - {py312,py313}-djangomain + ; {py312,py313}-djangomain py39-docs py39-lint From 49da77650134cf7e19544a9b6b3d5663a6ae380f Mon Sep 17 00:00:00 2001 From: Ilya Date: Mon, 1 Dec 2025 11:23:13 +0100 Subject: [PATCH 126/126] run tests for django 5.2 --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index a6ba9217..1c5bc493 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = {py39,py310,py311}-django42 {py310,py311,py312}-django50 {py310,py311,py312,py313}-django51 + {py310,py311,py312,py313}-django52 ; {py312,py313}-djangomain py39-docs py39-lint @@ -17,6 +18,7 @@ deps = django42: Django>=4.2,<4.3 django50: Django>=5.0,<5.1 django51: Django>=5.1,<5.2 + django52: Django>=5.2,<6.0 djangomain: https://github.com/django/django/archive/main.tar.gz # Test requirements coverage