From 8fe776ae45964d20bf113d5bff9287398f89480a Mon Sep 17 00:00:00 2001 From: Robin Harms Oredsson Date: Fri, 4 Nov 2022 09:12:06 +0100 Subject: [PATCH 1/5] 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 ca6a872..d87b91e 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 fdb685c..4df8d87 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -15,3 +15,8 @@ settings.AUDITLOG_EXCLUDE_TRACKING_MODELS = getattr( 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 6e9513d..de2c0ca 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 b6e867d..8117870 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 d2ab0b0..3bf3653 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -266,7 +266,8 @@ class AuditlogModelRegistry: """ 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 0000000..c5c5d9e --- /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 9205902..b43faaa 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -12,12 +12,13 @@ from django.contrib.admin.sites import AdminSite 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 @@ class RegisterModelSettingsTest(TestCase): ): 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 @@ class TestModelSerialization(TestCase): "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 00abffd..0675283 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 2/5] 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 d87b91e..f31dc4c 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 0000000..7627941 --- /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 efa1ab5..5636480 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -12,6 +12,7 @@ from django.utils.timezone import localtime from auditlog.models import LogEntry from auditlog.registry import auditlog +from auditlog.signals import accessed MAX = 75 @@ -50,7 +51,7 @@ class LogEntryAdminMixin: @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 @@ class LogEntryAdminMixin: 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 63c154f..aefc56a 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -308,17 +308,20 @@ class LogEntry(models.Model): 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 8117870..2a2c475 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 3bf3653..0d76455 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -24,6 +24,7 @@ from django.db.models.signals import ( ) from auditlog.conf import settings +from auditlog.signals import accessed DispatchUID = Tuple[int, int, int] @@ -44,10 +45,11 @@ class AuditlogModelRegistry: 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 @@ class AuditlogModelRegistry: 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 0000000..67e518c --- /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 0000000..e69de29 diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index b43faaa..3f60f05 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -15,6 +15,7 @@ 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.urls import reverse from django.utils import dateformat, formats, timezone from auditlog.admin import LogEntryAdmin @@ -1806,6 +1807,33 @@ class TestModelSerialization(TestCase): ) +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 083932c..712071d 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 0000000..436ecbf --- /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 0675283..9e81641 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 3/5] 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 1eae44a..d9a12fe 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 f31dc4c..0418bd1 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 9135b24..e5e45ca 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ setup( "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 e7fc098..34ce2b4 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 4/5] 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 0418bd1..ac9cef7 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 74bd9a0..e657ee1 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 aefc56a..e073c88 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 import serializers 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 3f60f05..2679efc 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -2,6 +2,7 @@ import datetime import itertools import json import warnings +from datetime import timezone from unittest import mock import freezegun @@ -16,7 +17,8 @@ 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 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 @@ class AdditionalDataModelTest(TestCase): 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 @@ class DateTimeFieldModelTest(TestCase): " 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 @@ class DateTimeFieldModelTest(TestCase): 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 @@ class ModelInstanceDiffTest(TestCase): 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 @@ class DisableTest(TestCase): 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 @@ class DisableTest(TestCase): 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 5/5] Prepare release 2.2.0 (#434) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac9cef7..174e6e9 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))