diff --git a/README.md b/README.md index 1824232e..5322946c 100644 --- a/README.md +++ b/README.md @@ -198,9 +198,9 @@ There are separate Django management commands for each task: ```` * Remove user accounts that match either of these conditions: - * Account was created more than a month ago, and user has never logged in. + * Account was created more than 6 months ago, and user has never logged in. These can happen when user enters invalid email address when signing up. - * Last login was more than a month ago, and the account has no checks. + * Last login was more than 6 months ago, and the account has no checks. Assume the user doesn't intend to use the account any more and would probably *want* it removed. diff --git a/hc/accounts/management/commands/pruneusers.py b/hc/accounts/management/commands/pruneusers.py index 055d3c46..d17f8f12 100644 --- a/hc/accounts/management/commands/pruneusers.py +++ b/hc/accounts/management/commands/pruneusers.py @@ -10,28 +10,32 @@ class Command(BaseCommand): help = """Prune old, inactive user accounts. Conditions for removing an user account: - - created 1+ month ago and never logged in. + - created 6 months ago and never logged in. Does not belong + to any team. Use case: visitor types in their email at the website but never follows through with login. - - not logged in for 1 month, and has no checks + - not logged in for 6 months, and has no checks. Does not + belong to any team. Use case: user wants to remove their account. So they remove all checks and leave the account at that. """ def handle(self, *args, **options): - cutoff = timezone.now() - timedelta(days=31) + cutoff = timezone.now() - timedelta(days=180) - # Old accounts, never logged in + # Old accounts, never logged in, no team memberships q = User.objects - q = q.filter(date_joined__lt=cutoff, last_login=None) + q = q.annotate(n_teams=Count("member")) + q = q.filter(date_joined__lt=cutoff, last_login=None, n_teams=0) n1, _ = q.delete() - # Not logged in for 1 month, 0 checks + # Not logged in for 1 month, 0 checks, no team memberships q = User.objects q = q.annotate(n_checks=Count("check")) - q = q.filter(last_login__lt=cutoff, n_checks=0) + q = q.annotate(n_teams=Count("member")) + q = q.filter(last_login__lt=cutoff, n_checks=0, n_teams=0) n2, _ = q.delete() return "Done! Pruned %d user accounts." % (n1 + n2) diff --git a/hc/accounts/tests/test_pruneusers.py b/hc/accounts/tests/test_pruneusers.py new file mode 100644 index 00000000..546ba8a6 --- /dev/null +++ b/hc/accounts/tests/test_pruneusers.py @@ -0,0 +1,40 @@ +from datetime import timedelta + +from django.contrib.auth.models import User +from django.utils import timezone +from hc.accounts.management.commands.pruneusers import Command +from hc.api.models import Check +from hc.test import BaseTestCase + + +class PruneUsersTestCase(BaseTestCase): + year_ago = timezone.now() - timedelta(days=365) + + def test_it_removes_old_never_logged_in_users(self): + self.charlie.date_joined = self.year_ago + self.charlie.save() + + # Charlie has one demo check + Check(user=self.charlie).save() + + Command().handle() + + self.assertEqual(User.objects.filter(username="charlie").count(), 0) + self.assertEqual(Check.objects.count(), 0) + + def test_it_removes_old_users_with_zero_checks(self): + self.charlie.date_joined = self.year_ago + self.charlie.last_login = self.year_ago + self.charlie.save() + + Command().handle() + self.assertEqual(User.objects.filter(username="charlie").count(), 0) + + def test_it_leaves_team_members_alone(self): + self.bob.date_joined = self.year_ago + self.bob.last_login = self.year_ago + self.bob.save() + + Command().handle() + # Bob belongs to a team so should not get removed + self.assertEqual(User.objects.filter(username="bob").count(), 1)