Skip to content

Commit b1ecc8f

Browse files
authored
feat: Add truncate option to flush command (#681)
* [feat]: Add truncate option to flush command * [test]: Add test cases for flush command with truncate * [refactor]: Simplified truncate query class and remove redundant return statement * [test]: Add test cases to test truncate for unsupported database vendor * [docs]: Update change log
1 parent 512cd28 commit b1ecc8f

File tree

3 files changed

+126
-4
lines changed

3 files changed

+126
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#### Improvements
66

77
- feat: Added `LogEntry.remote_port` field. ([#671](https://github.com/jazzband/django-auditlog/pull/671))
8+
- feat: Added `truncate` option to `auditlogflush` management command. ([#681](https://github.com/jazzband/django-auditlog/pull/681))
89
- Drop Python 3.8 support. ([#678](https://github.com/jazzband/django-auditlog/pull/678))
910
- Confirm Django 5.1 support and drop Django 3.2 support. ([#677](https://github.com/jazzband/django-auditlog/pull/677))
1011

auditlog/management/commands/auditlogflush.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22

33
from django.core.management.base import BaseCommand
4+
from django.db import connection
45

56
from auditlog.models import LogEntry
67

@@ -25,11 +26,24 @@ def add_arguments(self, parser):
2526
dest="before_date",
2627
type=datetime.date.fromisoformat,
2728
)
29+
parser.add_argument(
30+
"-t",
31+
"--truncate",
32+
action="store_true",
33+
default=None,
34+
help="Truncate log entry table.",
35+
dest="truncate",
36+
)
2837

2938
def handle(self, *args, **options):
3039
answer = options["yes"]
40+
truncate = options["truncate"]
3141
before = options["before_date"]
32-
42+
if truncate and before:
43+
self.stdout.write(
44+
"Truncate deletes all log entries and can not be passed with before-date."
45+
)
46+
return
3347
if answer is None:
3448
warning_message = (
3549
"This action will clear all log entries from the database."
@@ -42,11 +56,39 @@ def handle(self, *args, **options):
4256
)
4357
answer = response == "y"
4458

45-
if answer:
59+
if not answer:
60+
self.stdout.write("Aborted.")
61+
return
62+
63+
if not truncate:
4664
entries = LogEntry.objects.all()
4765
if before is not None:
4866
entries = entries.filter(timestamp__date__lt=before)
4967
count, _ = entries.delete()
5068
self.stdout.write("Deleted %d objects." % count)
5169
else:
52-
self.stdout.write("Aborted.")
70+
database_vendor = connection.vendor
71+
database_display_name = connection.display_name
72+
table_name = LogEntry._meta.db_table
73+
if not TruncateQuery.support_truncate_statement(database_vendor):
74+
self.stdout.write(
75+
"Database %s does not support truncate statement."
76+
% database_display_name
77+
)
78+
return
79+
with connection.cursor() as cursor:
80+
query = TruncateQuery.to_sql(table_name)
81+
cursor.execute(query)
82+
self.stdout.write("Truncated log entry table.")
83+
84+
85+
class TruncateQuery:
86+
SUPPORTED_VENDORS = ("postgresql", "mysql", "sqlite", "oracle", "microsoft")
87+
88+
@classmethod
89+
def support_truncate_statement(cls, database_vendor) -> bool:
90+
return database_vendor in cls.SUPPORTED_VENDORS
91+
92+
@staticmethod
93+
def to_sql(table_name) -> str:
94+
return f"TRUNCATE TABLE {table_name};"

auditlog_tests/test_commands.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import freezegun
88
from django.core.management import call_command
9-
from django.test import TestCase
9+
from django.test import TestCase, TransactionTestCase
1010

1111
from auditlog_tests.models import SimpleModel
1212

@@ -110,3 +110,82 @@ def test_before_date(self):
110110
out, "Deleted 1 objects.", msg="Output shows deleted 1 object."
111111
)
112112
self.assertEqual(err, "", msg="No stderr")
113+
114+
115+
class AuditlogFlushWithTruncateTest(TransactionTestCase):
116+
def setUp(self):
117+
input_patcher = mock.patch("builtins.input")
118+
self.mock_input = input_patcher.start()
119+
self.addCleanup(input_patcher.stop)
120+
121+
def make_object(self):
122+
return SimpleModel.objects.create(text="I am a simple model.")
123+
124+
def call_command(self, *args, **kwargs):
125+
outbuf = StringIO()
126+
errbuf = StringIO()
127+
call_command("auditlogflush", *args, stdout=outbuf, stderr=errbuf, **kwargs)
128+
return outbuf.getvalue().strip(), errbuf.getvalue().strip()
129+
130+
def test_flush_with_both_truncate_and_before_date_options(self):
131+
obj = self.make_object()
132+
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
133+
out, err = self.call_command("--truncate", "--before-date=2000-01-01")
134+
135+
self.assertEqual(obj.history.count(), 1, msg="There is still one log entry.")
136+
self.assertEqual(
137+
out,
138+
"Truncate deletes all log entries and can not be passed with before-date.",
139+
msg="Output shows error",
140+
)
141+
self.assertEqual(err, "", msg="No stderr")
142+
143+
def test_flush_with_truncate_and_yes(self):
144+
obj = self.make_object()
145+
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
146+
out, err = self.call_command("--truncate", "--y")
147+
148+
self.assertEqual(obj.history.count(), 0, msg="There is no log entry.")
149+
self.assertEqual(
150+
out,
151+
"Truncated log entry table.",
152+
msg="Output shows table gets truncate",
153+
)
154+
self.assertEqual(err, "", msg="No stderr")
155+
156+
def test_flush_with_truncate_with_input_yes(self):
157+
obj = self.make_object()
158+
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
159+
self.mock_input.return_value = "Y\n"
160+
out, err = self.call_command("--truncate")
161+
162+
self.assertEqual(obj.history.count(), 0, msg="There is no log entry.")
163+
self.assertEqual(
164+
out,
165+
"This action will clear all log entries from the database.\nTruncated log entry table.",
166+
msg="Output shows warning and table gets truncate",
167+
)
168+
self.assertEqual(err, "", msg="No stderr")
169+
170+
@mock.patch(
171+
"django.db.connection.vendor",
172+
new_callable=mock.PropertyMock(return_value="unknown"),
173+
)
174+
@mock.patch(
175+
"django.db.connection.display_name",
176+
new_callable=mock.PropertyMock(return_value="Unknown"),
177+
)
178+
def test_flush_with_truncate_for_unsupported_database_vendor(
179+
self, mocked_vendor, mocked_db_name
180+
):
181+
obj = self.make_object()
182+
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
183+
out, err = self.call_command("--truncate", "--y")
184+
185+
self.assertEqual(obj.history.count(), 1, msg="There is still one log entry.")
186+
self.assertEqual(
187+
out,
188+
"Database Unknown does not support truncate statement.",
189+
msg="Output shows error",
190+
)
191+
self.assertEqual(err, "", msg="No stderr")

0 commit comments

Comments
 (0)