Skip to content

Commit 785deba

Browse files
PauloPeresRoss Mechanic
andauthored
Added a Clean old history command to clean older history entries by cron/task (#675)
* Tests and Documentation Completed * Running Make Format * Updating Authors and Changes rst * Update docs/utils.rst fixing misspeling Co-authored-by: Ross Mechanic <[email protected]> * Update docs/utils.rst Accepting @rossmechanic changes Co-authored-by: Ross Mechanic <[email protected]> * Fixing Code Review from @rossmechanic Co-authored-by: Ross Mechanic <[email protected]>
1 parent 922f6ce commit 785deba

File tree

6 files changed

+300
-1
lines changed

6 files changed

+300
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ MANIFEST
1717
test_files/
1818
venv/
1919
.DS_Store
20+
env
21+
.vscode

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Authors
9999
- Ulysses Vilela
100100
- `vnagendra <https://github.com/vnagendra>`_
101101
- `yakimka <https://github.com/yakimka>`_
102+
- `Paulo Peres <https://github.com/PauloPeres>`_
102103

103104
Background
104105
==========

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
Changes
22
=======
3+
2.10.1 (2020-06-16)
4+
- added ``clean_old_history`` management command (gh-675)
35

46
2.10.0 (2020-04-27)
57
-------------------

docs/utils.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,26 @@ so you can schedule, for instance, an hourly cronjob such as
2323
.. code-block:: bash
2424
2525
$ python manage.py clean_duplicate_history -m 60 --auto
26+
27+
28+
clean_old_history
29+
-----------------------
30+
31+
You may want to remove historical records that have existed for a certain amount of time.
32+
33+
If you find yourself with a lot of old history you can schedule the
34+
``clean_old_history`` command
35+
36+
.. code-block:: bash
37+
38+
$ python manage.py clean_old_history --auto
39+
40+
You can use ``--auto`` to remove old historial entries
41+
with ``HistoricalRecords`` or enumerate specific models as args.
42+
You may also specify a ``--days`` parameter, which indicates how many
43+
days of records you want to keep. The default it 30 days, meaning that
44+
all records older than 30 days would be removed.
45+
46+
.. code-block:: bash
47+
48+
$ python manage.py clean_old_history --days 60 --auto
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from django.utils import timezone
2+
from django.db import transaction
3+
4+
from . import populate_history
5+
from ... import models, utils
6+
from ...exceptions import NotHistoricalModelError
7+
8+
9+
class Command(populate_history.Command):
10+
args = "<app.model app.model ...>"
11+
help = "Scans HistoricalRecords for old entries " "and deletes them."
12+
13+
DONE_CLEANING_FOR_MODEL = "Removed {count} historical records for {model}\n"
14+
15+
def add_arguments(self, parser):
16+
parser.add_argument("models", nargs="*", type=str)
17+
parser.add_argument(
18+
"--auto",
19+
action="store_true",
20+
dest="auto",
21+
default=False,
22+
help="Automatically search for models with the HistoricalRecords field "
23+
"type",
24+
)
25+
parser.add_argument(
26+
"--days",
27+
help="Only Keep the last X Days of history, default is 30",
28+
action="store_true",
29+
dest="days",
30+
default=30,
31+
)
32+
33+
parser.add_argument(
34+
"-d", "--dry", action="store_true", help="Dry (test) run only, no changes"
35+
)
36+
37+
def handle(self, *args, **options):
38+
self.verbosity = options["verbosity"]
39+
40+
to_process = set()
41+
model_strings = options.get("models", []) or args
42+
43+
if model_strings:
44+
for model_pair in self._handle_model_list(*model_strings):
45+
to_process.add(model_pair)
46+
47+
elif options["auto"]:
48+
to_process = self._auto_models()
49+
50+
else:
51+
self.log(self.COMMAND_HINT)
52+
53+
self._process(to_process, days_back=options["days"], dry_run=options["dry"])
54+
55+
def _process(self, to_process, days_back=None, dry_run=True):
56+
57+
start_date = timezone.now() - timezone.timedelta(days=days_back)
58+
for model, history_model in to_process:
59+
history_model_manager = history_model.objects
60+
history_model_manager = history_model_manager.filter(
61+
history_date__lt=start_date
62+
)
63+
found = len(history_model_manager)
64+
self.log("{0} has {1} old historical entries".format(model, found), 2)
65+
if not found:
66+
continue
67+
if not dry_run:
68+
history_model_manager.delete()
69+
70+
self.log(self.DONE_CLEANING_FOR_MODEL.format(model=model, count=found))
71+
72+
def log(self, message, verbosity_level=1):
73+
if self.verbosity >= verbosity_level:
74+
self.stdout.write(message)

simple_history/tests/tests/test_commands.py

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from six.moves import cStringIO as StringIO
77

88
from simple_history import models as sh_models
9-
from simple_history.management.commands import populate_history, clean_duplicate_history
9+
from simple_history.management.commands import (
10+
populate_history,
11+
clean_duplicate_history,
12+
clean_old_history,
13+
)
1014
from ..models import (
1115
Book,
1216
CustomManagerNameModel,
@@ -381,3 +385,196 @@ def test_auto_cleanup_custom_history_field(self):
381385
"<class 'simple_history.tests.models.CustomManagerNameModel'>\n",
382386
)
383387
self.assertEqual(CustomManagerNameModel.log.all().count(), 2)
388+
389+
390+
class TestCleanOldHistory(TestCase):
391+
command_name = "clean_old_history"
392+
command_error = (management.CommandError, SystemExit)
393+
394+
def test_no_args(self):
395+
out = StringIO()
396+
management.call_command(self.command_name, stdout=out, stderr=StringIO())
397+
self.assertIn(clean_old_history.Command.COMMAND_HINT, out.getvalue())
398+
399+
def test_bad_args(self):
400+
test_data = (
401+
(clean_old_history.Command.MODEL_NOT_HISTORICAL, ("tests.place",)),
402+
(clean_old_history.Command.MODEL_NOT_FOUND, ("invalid.model",)),
403+
(clean_old_history.Command.MODEL_NOT_FOUND, ("bad_key",)),
404+
)
405+
for msg, args in test_data:
406+
out = StringIO()
407+
self.assertRaises(
408+
self.command_error,
409+
management.call_command,
410+
self.command_name,
411+
*args,
412+
stdout=StringIO(),
413+
stderr=out
414+
)
415+
self.assertIn(msg, out.getvalue())
416+
417+
def test_no_historical(self):
418+
out = StringIO()
419+
with replace_registry({"test_place": Place}):
420+
management.call_command(self.command_name, auto=True, stdout=out)
421+
self.assertIn(clean_old_history.Command.NO_REGISTERED_MODELS, out.getvalue())
422+
423+
def test_auto_dry_run(self):
424+
p = Poll.objects.create(
425+
question="Will this be deleted?", pub_date=datetime.now()
426+
)
427+
p.save()
428+
429+
# not related to dry_run test, just for increasing coverage :)
430+
# create instance with single-entry history older than "minutes"
431+
# so it is skipped
432+
p = Poll.objects.create(
433+
question="Will this be deleted?", pub_date=datetime.now()
434+
)
435+
h = p.history.first()
436+
h.history_date -= timedelta(days=31)
437+
h.save()
438+
439+
self.assertEqual(Poll.history.all().count(), 3)
440+
out = StringIO()
441+
management.call_command(
442+
self.command_name,
443+
auto=True,
444+
days=20,
445+
dry=True,
446+
stdout=out,
447+
stderr=StringIO(),
448+
)
449+
self.assertEqual(
450+
out.getvalue(),
451+
"Removed 1 historical records for "
452+
"<class 'simple_history.tests.models.Poll'>\n",
453+
)
454+
self.assertEqual(Poll.history.all().count(), 3)
455+
456+
def test_auto_cleanup(self):
457+
p = Poll.objects.create(
458+
question="Will this be deleted?", pub_date=datetime.now()
459+
)
460+
self.assertEqual(Poll.history.all().count(), 1)
461+
p.save()
462+
self.assertEqual(Poll.history.all().count(), 2)
463+
p.question = "Maybe this one won't...?"
464+
p.save()
465+
self.assertEqual(Poll.history.all().count(), 3)
466+
out = StringIO()
467+
h = p.history.first()
468+
h.history_date -= timedelta(days=40)
469+
h.save()
470+
management.call_command(
471+
self.command_name, auto=True, stdout=out, stderr=StringIO()
472+
)
473+
self.assertEqual(
474+
out.getvalue(),
475+
"Removed 1 historical records for "
476+
"<class 'simple_history.tests.models.Poll'>\n",
477+
)
478+
self.assertEqual(Poll.history.all().count(), 2)
479+
480+
def test_auto_cleanup_verbose(self):
481+
p = Poll.objects.create(
482+
question="Will this be deleted?", pub_date=datetime.now()
483+
)
484+
self.assertEqual(Poll.history.all().count(), 1)
485+
p.save()
486+
p.question = "Maybe this one won't...?"
487+
p.save()
488+
h = p.history.first()
489+
h.history_date -= timedelta(days=40)
490+
h.save()
491+
self.assertEqual(Poll.history.all().count(), 3)
492+
out = StringIO()
493+
management.call_command(
494+
self.command_name,
495+
"tests.poll",
496+
auto=True,
497+
verbosity=2,
498+
stdout=out,
499+
stderr=StringIO(),
500+
)
501+
502+
self.assertEqual(
503+
out.getvalue(),
504+
"<class 'simple_history.tests.models.Poll'> has 1 old historical entries\n"
505+
"Removed 1 historical records for "
506+
"<class 'simple_history.tests.models.Poll'>\n",
507+
)
508+
self.assertEqual(Poll.history.all().count(), 2)
509+
510+
def test_auto_cleanup_dated(self):
511+
the_time_is_now = datetime.now()
512+
p = Poll.objects.create(
513+
question="Will this be deleted?", pub_date=the_time_is_now
514+
)
515+
self.assertEqual(Poll.history.all().count(), 1)
516+
p.save()
517+
p.save()
518+
self.assertEqual(Poll.history.all().count(), 3)
519+
p.question = "Or this one...?"
520+
p.save()
521+
p.save()
522+
self.assertEqual(Poll.history.all().count(), 5)
523+
524+
for h in Poll.history.all()[2:]:
525+
h.history_date -= timedelta(days=30)
526+
h.save()
527+
528+
management.call_command(
529+
self.command_name, auto=True, days=20, stdout=StringIO(), stderr=StringIO(),
530+
)
531+
self.assertEqual(Poll.history.all().count(), 2)
532+
533+
def test_auto_cleanup_dated_extra_one(self):
534+
the_time_is_now = datetime.now()
535+
p = Poll.objects.create(
536+
question="Will this be deleted?", pub_date=the_time_is_now
537+
)
538+
self.assertEqual(Poll.history.all().count(), 1)
539+
p.save()
540+
p.save()
541+
self.assertEqual(Poll.history.all().count(), 3)
542+
p.question = "Or this one...?"
543+
p.save()
544+
p.save()
545+
p.save()
546+
p.save()
547+
self.assertEqual(Poll.history.all().count(), 7)
548+
549+
for h in Poll.history.all()[2:]:
550+
h.history_date -= timedelta(days=30)
551+
h.save()
552+
553+
management.call_command(
554+
self.command_name, auto=True, days=20, stdout=StringIO(), stderr=StringIO(),
555+
)
556+
# We will remove the 3 ones that we are marking as old
557+
self.assertEqual(Poll.history.all().count(), 2)
558+
559+
def test_auto_cleanup_custom_history_field(self):
560+
m = CustomManagerNameModel.objects.create(name="John")
561+
self.assertEqual(CustomManagerNameModel.log.all().count(), 1)
562+
m.save()
563+
self.assertEqual(CustomManagerNameModel.log.all().count(), 2)
564+
m.name = "Ivan"
565+
m.save()
566+
h = m.log.first()
567+
h.history_date -= timedelta(days=40)
568+
h.save()
569+
self.assertEqual(CustomManagerNameModel.log.all().count(), 3)
570+
out = StringIO()
571+
management.call_command(
572+
self.command_name, auto=True, stdout=out, stderr=StringIO()
573+
)
574+
575+
self.assertEqual(
576+
out.getvalue(),
577+
"Removed 1 historical records for "
578+
"<class 'simple_history.tests.models.CustomManagerNameModel'>\n",
579+
)
580+
self.assertEqual(CustomManagerNameModel.log.all().count(), 2)

0 commit comments

Comments
 (0)