Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create command to remove unused data by checking existing code #493

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/usage/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
84 changes: 84 additions & 0 deletions waffle/management/commands/waffle_delete_unused.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you going to do this? Perhaps allow them to be passed in as an optional argument?

continue
filename = pathlib.Path(root) / file
if expression_in_file(expression, filename):
return True
return False
43 changes: 42 additions & 1 deletion waffle/tests/test_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests aren't really testing the desired behavior—deleting flags. Consider updating the command to take a file path of code to search, and passing that in the test calls to confirm items are deleted.

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())
Loading