diff --git a/docs/usage/cli.rst b/docs/usage/cli.rst index 96bbfaa8..b03bb73d 100644 --- a/docs/usage/cli.rst +++ b/docs/usage/cli.rst @@ -81,3 +81,14 @@ Use ``manage.py`` to delete a batch of flags, switches, and/or samples:: $ ./manage.py waffle_delete --switches switch_name_0 switch_name_1 --flags flag_name_0 flag_name_1 --samples sample_name_0 sample_name_1 Pass a list of switch, flag, or sample names to the command as keyword arguments and they will be deleted from the database. + +Deleting Unused Data +==================== + +Use ``manage.py`` to delete all flags, switches, and/or samples that are not used in any flag, switch, or sample objects:: + + $ ./manage.py waffle_delete_unused --switches --flags --samples + +To by pass the confirmation prompt, use the ``--noinput`` flag:: + + $ ./manage.py waffle_delete_unused --switches --flags --samples --no-input \ No newline at end of file diff --git a/waffle/management/commands/waffle_delete_unused.py b/waffle/management/commands/waffle_delete_unused.py new file mode 100644 index 00000000..93d79161 --- /dev/null +++ b/waffle/management/commands/waffle_delete_unused.py @@ -0,0 +1,84 @@ +import os +import pathlib + +from django.core.management.base import BaseCommand +from waffle import ( + get_waffle_flag_model, + get_waffle_switch_model, + get_waffle_sample_model, +) + + +class Command(BaseCommand): + help = "Delete flags, samples, and switches not present in the code from the Database" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not delete anything, just show what would be deleted", + ) + parser.add_argument( + "--no-input", + action="store_true", + help="Do not prompt for confirmation", + ) + parser.add_argument( + "--switches", + action="store_true", + help="Remove unused switches", + ) + parser.add_argument( + "--flags", + action="store_true", + help="Remove unused flags", + ) + parser.add_argument( + "--samples", + action="store_true", + help="Remove unused samples", + ) + + def handle(self, *args, **kwargs): + no_input = kwargs["no_input"] + delete_switches = kwargs["switches"] + delete_flags = kwargs["flags"] + delete_samples = kwargs["samples"] + if delete_switches: + self.delete_model(get_waffle_switch_model(), no_input) + if delete_flags: + self.delete_model(get_waffle_flag_model(), no_input) + if delete_samples: + self.delete_model(get_waffle_sample_model(), no_input) + + def delete_model(self, model, no_input): + items = model.objects.all() + for item in items: + if not expression_exists(item.name): + self.stdout.write("%s %s not found in the code" % (model.__name__, item.name)) + if no_input or self.confirm("Delete %s ?" % model.__name__): + self.stdout.write("Deleting switch") + item.delete() + else: + self.stdout.write("%s %s found in the code" % (model.__name__, item.name)) + + def confirm(self, question): + answer = input(question + " [y/N] ").strip() + return answer.lower() == "y" + + +def expression_in_file(expression, filename): + with open(filename) as file: + content = file.read() + return expression in content + + +def expression_exists(expression): + for root, dirs, files in os.walk(os.getcwd()): + for file in files: + if not (file.endswith(".py") or file.endswith(".html")): #TODO: make this a list of extensions + continue + filename = pathlib.Path(root) / file + if expression_in_file(expression, filename): + return True + return False diff --git a/waffle/tests/test_management.py b/waffle/tests/test_management.py index a70f3717..e7f9c6d4 100644 --- a/waffle/tests/test_management.py +++ b/waffle/tests/test_management.py @@ -290,7 +290,7 @@ def test_delete_flag(self): call_command('waffle_delete', flag_names=[name]) self.assertEqual(get_waffle_flag_model().objects.count(), 0) - def test_delete_swtich(self): + def test_delete_switch(self): """ The command should delete a switch. """ name = 'test_switch' get_waffle_switch_model().objects.create(name=name) @@ -329,3 +329,44 @@ def test_delete_some_but_not_all_records(self): call_command('waffle_delete', flag_names=[flag_1]) self.assertTrue(get_waffle_flag_model().objects.filter(name=flag_2).exists()) + + +class WaffleDeleteUnused(TestCase): + def test_delete_switches(self): + # we concat the strings so that the switch is not found in the code + get_waffle_switch_model().objects.create(name="FIRST" + "NEVER_FOUND" + "SWITCH", active=True) + get_waffle_switch_model().objects.create(name="SECOND" + "NEVER_FOUND" + "SWITCH", active=True) + # this test is in the search directory, so this very instance will be found + get_waffle_switch_model().objects.create(name="SWITCH_FOUND", active=True) + call_command('waffle_delete_unused', "--no-input", "--switches") + self.assertEqual(1, get_waffle_switch_model().objects.all().count()) + + def test_delete_samples(self): + # we concat the strings so that the switch is not found in the code + get_waffle_sample_model().objects.create(name="FIRST" + "NEVER_FOUND" + "SAMPLE", percent=0) + get_waffle_sample_model().objects.create(name="SECOND" + "NEVER_FOUND" + "SAMPLE", percent=0) + # this test is in the search directory, so this very instance will be found + get_waffle_sample_model().objects.create(name="SAMPLE_FOUND", percent=0) + call_command('waffle_delete_unused', "--no-input", "--samples") + self.assertEqual(1, get_waffle_sample_model().objects.all().count()) + + def test_delete_flags(self): + # we concat the strings so that the switch is not found in the code + get_waffle_flag_model().objects.create(name="FIRST" + "NEVER_FOUND" + "FLAG") + get_waffle_flag_model().objects.create(name="SECOND" + "NEVER_FOUND" + "FLAG") + # this test is in the search directory, so this very instance will be found + get_waffle_flag_model().objects.create(name="FLAG_FOUND") + call_command('waffle_delete_unused', "--no-input", "--flags") + self.assertEqual(1, get_waffle_flag_model().objects.all().count()) + + def test_deletion_confirmation(self): + from unittest import mock + mock.patch('builtins.input', side_effect=["N\n", "Y\n", "N\n"]).start() + + get_waffle_switch_model().objects.create(name="FIRST" + "NEVER_FOUND" + "SWITCH", active=True) + get_waffle_sample_model().objects.create(name="FIRST" + "NEVER_FOUND" + "SAMPLE", percent=0) + get_waffle_flag_model().objects.create(name="FIRST" + "NEVER_FOUND" + "FLAG") + call_command('waffle_delete_unused', "--flags", "--samples", "--switches") + self.assertEqual(0, get_waffle_flag_model().objects.all().count()) + self.assertEqual(1, get_waffle_sample_model().objects.all().count()) + self.assertEqual(1, get_waffle_switch_model().objects.all().count())