diff --git a/CHANGELOG.md b/CHANGELOG.md index ea68d16..c9719c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Improvements +- feat: Add `--before-date` option to `auditlogflush` to support retention windows ([#365](https://github.com/jazzband/django-auditlog/pull/365)) - feat: Add db_index to the `LogEntry.timestamp` column ([#364](https://github.com/jazzband/django-auditlog/pull/364)) - feat: Add register model from settings ([#368](https://github.com/jazzband/django-auditlog/pull/368)) - Context manager set_actor() for use in Celery tasks ([#262](https://github.com/jazzband/django-auditlog/pull/262)) @@ -15,12 +16,14 @@ ## 2.0.0 (2022-05-09) #### Improvements + - feat: enable use of replica database (delegating the choice to `DATABASES_ROUTER`) ([#359](https://github.com/jazzband/django-auditlog/pull/359)) - Add `mask_fields` argument in `register` to mask sensitive information when logging ([#310](https://github.com/jazzband/django-auditlog/pull/310)) - Django: Drop 2.2 support. `django_jsonfield_backport` is not required anymore ([#370](https://github.com/jazzband/django-auditlog/pull/370)) - Remove `default_app_config` configuration ([#372](https://github.com/jazzband/django-auditlog/pull/372)) #### Important notes + - LogEntry no longer save to same database instance is using ## 1.0.0 (2022-01-24) @@ -54,7 +57,6 @@ - Support Django's save method `update_fields` kwarg ([#336](https://github.com/jazzband/django-auditlog/pull/336)) - Fix invalid escape sequence on Python 3.7 - ### Alpha 1 (1.0a1, 2020-09-07) #### Improvements @@ -69,14 +71,12 @@ - Fix field choices diff - Allow higher versions of python-dateutil than 2.6.0 - ## 0.4.8 (2019-11-12) ### Improvements - Add support for PostgreSQL 10 - ## 0.4.7 (2019-12-19) ### Improvements @@ -85,7 +85,6 @@ - Django: add 2.1 and 2.2 support, drop < 1.11 versions - Python: add 3.7 support - ## 0.4.6 (2018-09-18) ### Features @@ -102,14 +101,12 @@ - Fix the rendering of the `msg` field with Django 2.0 ([#166](https://github.com/jazzband/django-auditlog/pull/166)) - Mark `LogEntryAdminMixin` methods output as safe where required ([#167](https://github.com/jazzband/django-auditlog/pull/167)) - ## 0.4.5 (2018-01-12) ### Improvements Added support for Django 2.0, along with a number of bug fixes. - ## 0.4.4 (2017-11-17) ### Improvements @@ -126,14 +123,12 @@ Added support for Django 2.0, along with a number of bug fixes. - Add management commands package to setup.py ([#130](https://github.com/jazzband/django-auditlog/pull/130)) - Add `changes_display_dict` property to `LogEntry` model to display diff in a more human readable format ([#94](https://github.com/jazzband/django-auditlog/pull/94)) - ## 0.4.3 (2017-02-16) ### Fixes - Fixes cricital bug in admin mixin making the library only usable on Django 1.11 - ## 0.4.2 (2017-02-16) _As it turns out, haste is never good. Due to the focus on quickly releasing this version a nasty bug was not spotted, which makes this version only usable with Django 1.11 and above. Upgrading to 0.4.3 is not only encouraged but most likely necessary. Apologies for the inconvenience and lacking quality control._ @@ -147,7 +142,6 @@ _As it turns out, haste is never good. Due to the focus on quickly releasing thi - A lot, yes, [_really_ a lot](https://github.com/jjkester/django-auditlog/milestone/8?closed=1), of fixes for the admin integration - Flush command fixed for Django 1.10 - ## 0.4.1 (2016-12-27) ### Improvements @@ -158,7 +152,6 @@ _As it turns out, haste is never good. Due to the focus on quickly releasing thi - Fixed multithreading issue where the wrong user was written to the log - ## 0.4.0 (2016-08-17) ### Breaking changes @@ -179,7 +172,6 @@ _As it turns out, haste is never good. Due to the focus on quickly releasing thi - Solved migration error for MySQL users - ## 0.3.3 (2016-01-23) ### Fixes @@ -192,7 +184,6 @@ _As it turns out, haste is never good. Due to the focus on quickly releasing thi - The `object_pk` field is now limited to 255 chars - ## 0.3.2 (2015-10-19) ### New functionality @@ -203,14 +194,12 @@ _As it turns out, haste is never good. Due to the focus on quickly releasing thi - Enhanced performance for non-integer primary key lookups - ## 0.3.1 (2015-07-29) ### Fixes - Auditlog data is now correctly stored in the thread. - ## 0.3.0 (2015-07-22) ### Breaking changes @@ -231,14 +220,12 @@ _As it turns out, haste is never good. Due to the focus on quickly releasing thi - Better documentation - Compatibility with [django-polymorphic](https://pypi.org/project/django-polymorphic/) - ## 0.2.1 (2014-07-08) ### New functionality - South compatibility for `AuditlogHistoryField` - ## 0.2.0 (2014-03-08) Although this release contains mostly bugfixes, the improvements were significant enough to justify a higher version number. @@ -249,7 +236,6 @@ Although this release contains mostly bugfixes, the improvements were significan - Model diffs use unicode strings instead of regular strings - Tests on middleware - ## 0.1.1 (2013-12-12) ### New functionality @@ -261,7 +247,6 @@ Although this release contains mostly bugfixes, the improvements were significan - Only save a new log entry if there are actual changes - Better way of loading the user model in the middleware - ## 0.1.0 (2013-10-21) First beta release of Auditlog. diff --git a/auditlog/management/commands/auditlogflush.py b/auditlog/management/commands/auditlogflush.py index a2becd4..a5b1397 100644 --- a/auditlog/management/commands/auditlogflush.py +++ b/auditlog/management/commands/auditlogflush.py @@ -1,3 +1,5 @@ +import datetime + from django.core.management.base import BaseCommand from auditlog.models import LogEntry @@ -8,27 +10,43 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - "-y, --yes", + "-y", + "--yes", action="store_true", default=None, help="Continue without asking confirmation.", dest="yes", ) + parser.add_argument( + "-b", + "--before-date", + default=None, + help="Flush all entries with a timestamp before a given date (ISO 8601).", + dest="before_date", + type=datetime.date.fromisoformat, + ) def handle(self, *args, **options): answer = options["yes"] + before = options["before_date"] if answer is None: - self.stdout.write( + warning_message = ( "This action will clear all log entries from the database." ) + if before is not None: + warning_message = f"This action will clear all log entries before {before} from the database." + self.stdout.write(warning_message) response = ( input("Are you sure you want to continue? [y/N]: ").lower().strip() ) answer = response == "y" if answer: - count, _ = LogEntry.objects.all().delete() + entries = LogEntry.objects.all() + if before is not None: + entries = entries.filter(timestamp__lt=before) + count, _ = entries.delete() self.stdout.write("Deleted %d objects." % count) else: self.stdout.write("Aborted.") diff --git a/auditlog_tests/test_commands.py b/auditlog_tests/test_commands.py new file mode 100644 index 0000000..c7ec41b --- /dev/null +++ b/auditlog_tests/test_commands.py @@ -0,0 +1,109 @@ +"""Tests for auditlog.management.commands""" + +import datetime +from io import StringIO +from unittest import mock + +import freezegun +from django.core.management import call_command +from django.test import TestCase + +from auditlog_tests.models import SimpleModel + + +class AuditlogFlushTest(TestCase): + def setUp(self): + input_patcher = mock.patch("builtins.input") + self.mock_input = input_patcher.start() + self.addCleanup(input_patcher.stop) + + def make_object(self): + return SimpleModel.objects.create(text="I am a simple model.") + + def call_command(self, *args, **kwargs): + outbuf = StringIO() + errbuf = StringIO() + call_command("auditlogflush", *args, stdout=outbuf, stderr=errbuf, **kwargs) + return outbuf.getvalue().strip(), errbuf.getvalue().strip() + + def test_flush_yes(self): + obj = self.make_object() + self.assertEqual(obj.history.count(), 1, msg="There is one log entry.") + + out, err = self.call_command("--yes") + + self.assertEqual(obj.history.count(), 0, msg="There are no log entries.") + self.assertEqual( + out, "Deleted 1 objects.", msg="Output shows deleted 1 object." + ) + self.assertEqual(err, "", msg="No stderr") + + def test_flush_no(self): + obj = self.make_object() + self.assertEqual(obj.history.count(), 1, msg="There is one log entry.") + + self.mock_input.return_value = "N\n" + out, err = self.call_command() + + self.assertEqual(obj.history.count(), 1, msg="There is still one log entry.") + self.assertEqual( + out, + "This action will clear all log entries from the database.\nAborted.", + msg="Output shows warning and aborted.", + ) + self.assertEqual(err, "", msg="No stderr") + + def test_flush_input_yes(self): + obj = self.make_object() + self.assertEqual(obj.history.count(), 1, msg="There is one log entry.") + + self.mock_input.return_value = "Y\n" + out, err = self.call_command() + + self.assertEqual(obj.history.count(), 0, msg="There are no log entries.") + self.assertEqual( + out, + "This action will clear all log entries from the database.\nDeleted 1 objects.", + msg="Output shows warning and deleted 1 object.", + ) + self.assertEqual(err, "", msg="No stderr") + + def test_before_date_input(self): + self.mock_input.return_value = "N\n" + out, err = self.call_command("--before-date=2000-01-01") + self.assertEqual( + out, + "This action will clear all log entries before 2000-01-01 from the database.\nAborted.", + msg="Output shows warning with date and then aborted.", + ) + self.assertEqual(err, "", msg="No stderr") + + def test_before_date(self): + with freezegun.freeze_time("1999-12-31"): + obj = self.make_object() + + with freezegun.freeze_time("2000-01-02"): + obj.text = "I have new text" + obj.save() + + self.assertEqual( + {v["timestamp"] for v in obj.history.values("timestamp")}, + { + datetime.datetime(1999, 12, 31, tzinfo=datetime.timezone.utc), + datetime.datetime(2000, 1, 2, tzinfo=datetime.timezone.utc), + }, + msg="Entries exist for 1999-12-31 and 2000-01-02", + ) + + out, err = self.call_command("--yes", "--before-date=2000-01-01") + self.assertEqual( + {v["timestamp"] for v in obj.history.values("timestamp")}, + { + datetime.datetime(2000, 1, 2, tzinfo=datetime.timezone.utc), + }, + msg="An entry exists only for 2000-01-02", + ) + self.assertEqual( + out, "Deleted 1 objects.", msg="Output shows deleted 1 object." + ) + self.assertEqual(err, "", msg="No stderr") diff --git a/docs/source/usage.rst b/docs/source/usage.rst index d0b3649..5696a1d 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -56,15 +56,15 @@ If you have field names on your models that aren't intuitive or user friendly yo during the `register()` call. .. code-block:: python - + class MyModel(modelsModel): sku = models.CharField(max_length=20) version = models.CharField(max_length=5) product = models.CharField(max_length=50, verbose_name='Product Name') history = AuditlogHistoryField() - + auditlog.register(MyModel, mapping_fields={'sku': 'Product No.', 'version': 'Product Revision'}) - + .. code-block:: python log = MyModel.objects.first().history.latest() @@ -279,12 +279,17 @@ Management commands Auditlog provides the ``auditlogflush`` management command to clear all log entries from the database. -By default, the command asks for confirmation. It is possible to run the command with the `-y` or `--yes` flag to skip +By default, the command asks for confirmation. It is possible to run the command with the ``-y`` or ``--yes`` flag to skip confirmation and immediately delete all entries. +You may also specify a date using the ``-b`` or ``--before-date`` option in ISO 8601 format (YYYY-mm-dd) to delete all +log entries prior to a given date. This may be used to implement time based retention windows. + +.. versionadded:: 2.1.0 + .. warning:: - Using the ``auditlogflush`` command deletes all log entries permanently and irreversibly from the database. + Using the ``auditlogflush`` command deletes log entries permanently and irreversibly from the database. Django Admin integration ------------------------ diff --git a/tox.ini b/tox.ini index 78124df..bda1a65 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ deps = # Test requirements coverage codecov + freezegun psycopg2-binary==2.8.6 passenv= TEST_DB_HOST