From 15ba4152987e58d65e992019b280d281ccbdf236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Wed, 11 Dec 2019 15:24:51 +0200 Subject: [PATCH] `senddeletionnotices` command skips profiles with recent last_active_date --- CHANGELOG.md | 1 + .../commands/senddeletionnotices.py | 17 ++- hc/accounts/tests/test_senddeletionnotices.py | 128 ++++++++++++++++++ hc/api/management/commands/sendreports.py | 1 - 4 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 hc/accounts/tests/test_senddeletionnotices.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 061a4c77..a4686813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Show Healthchecks version in Django admin header (#306) - Added JSON endpoint for Shields.io (#304) - Django 3.0 +- `senddeletionnotices` command skips profiles with recent last_active_date ### Bug Fixes - Don't set CSRF cookie on first visit. Signup is exempt from CSRF protection diff --git a/hc/accounts/management/commands/senddeletionnotices.py b/hc/accounts/management/commands/senddeletionnotices.py index 465f752d..59c0da87 100644 --- a/hc/accounts/management/commands/senddeletionnotices.py +++ b/hc/accounts/management/commands/senddeletionnotices.py @@ -19,16 +19,21 @@ class Command(BaseCommand): """ + def pause(self): + time.sleep(1) + def handle(self, *args, **options): year_ago = now() - timedelta(days=365) q = Profile.objects.order_by("id") - # Exclude accounts with logins in the last year_ago + # Exclude accounts with logins in the last year q = q.exclude(user__last_login__gt=year_ago) - # Exclude accounts less than a year_ago old + # Exclude accounts less than a year old q = q.exclude(user__date_joined__gt=year_ago) # Exclude accounts with the deletion notice already sent q = q.exclude(deletion_notice_date__gt=year_ago) + # Exclude accounts with activity in the last year + q = q.exclude(last_active_date__gt=year_ago) # Exclude paid accounts q = q.exclude(sms_limit__gt=5) @@ -36,14 +41,14 @@ class Command(BaseCommand): for profile in q: members = Member.objects.filter(project__owner_id=profile.user_id) if members.exists(): - print("Skipping %s, has team members" % profile) + self.stdout.write("Skipping %s, has team members" % profile) continue pings = Ping.objects pings = pings.filter(owner__project__owner_id=profile.user_id) pings = pings.filter(created__gt=year_ago) if pings.exists(): - print("Skipping %s, has pings in last year" % profile) + self.stdout.write("Skipping %s, has pings in last year" % profile) continue self.stdout.write("Sending notice to %s" % profile.user.email) @@ -53,8 +58,10 @@ class Command(BaseCommand): ctx = {"email": profile.user.email, "support_email": settings.SUPPORT_EMAIL} emails.deletion_notice(profile.user.email, ctx) + # Throttle so we don't send too many emails at once: - time.sleep(1) + self.pause() + sent += 1 return "Done! Sent %d notices" % sent diff --git a/hc/accounts/tests/test_senddeletionnotices.py b/hc/accounts/tests/test_senddeletionnotices.py new file mode 100644 index 00000000..a9257dcf --- /dev/null +++ b/hc/accounts/tests/test_senddeletionnotices.py @@ -0,0 +1,128 @@ +from datetime import timedelta as td + +from django.core import mail +from django.utils.timezone import now +from hc.accounts.management.commands.senddeletionnotices import Command +from hc.accounts.models import Member +from hc.api.models import Check, Ping +from hc.test import BaseTestCase +from mock import Mock + + +class SendDeletionNoticesTestCase(BaseTestCase): + def setUp(self): + super(SendDeletionNoticesTestCase, self).setUp() + + # Make alice eligible for notice -- signed up more than 1 year ago + self.alice.date_joined = now() - td(days=500) + self.alice.save() + + self.profile.sms_limit = 5 + self.profile.save() + + # remove members from alice's project + self.project.member_set.all().delete() + + def test_it_sends_notice(self): + cmd = Command() + cmd.stdout = Mock() # silence output to stdout + cmd.pause = Mock() # don't pause for 1s + + result = cmd.handle() + self.assertEqual(result, "Done! Sent 1 notices") + + self.profile.refresh_from_db() + self.assertTrue(self.profile.deletion_notice_date) + + email = mail.outbox[0] + self.assertEqual(email.subject, "Inactive Account Notification") + + def test_it_checks_last_login(self): + # alice has logged in recently: + self.alice.last_login = now() - td(days=15) + self.alice.save() + + cmd = Command() + cmd.stdout = Mock() # silence output to stdout + + result = cmd.handle() + self.assertEqual(result, "Done! Sent 0 notices") + + self.profile.refresh_from_db() + self.assertIsNone(self.profile.deletion_notice_date) + + def test_it_checks_date_joined(self): + # alice signed up recently: + self.alice.date_joined = now() - td(days=15) + self.alice.save() + + cmd = Command() + cmd.stdout = Mock() # silence output to stdout + + result = cmd.handle() + self.assertEqual(result, "Done! Sent 0 notices") + + self.profile.refresh_from_db() + self.assertIsNone(self.profile.deletion_notice_date) + + def test_it_checks_deletion_notice_date(self): + # alice has already received a deletion notice + self.profile.deletion_notice_date = now() - td(days=15) + self.profile.save() + + cmd = Command() + cmd.stdout = Mock() # silence output to stdout + + result = cmd.handle() + self.assertEqual(result, "Done! Sent 0 notices") + + def test_it_checks_sms_limit(self): + # alice has a paid account + self.profile.sms_limit = 50 + self.profile.save() + + cmd = Command() + cmd.stdout = Mock() # silence output to stdout + + result = cmd.handle() + self.assertEqual(result, "Done! Sent 0 notices") + + self.profile.refresh_from_db() + self.assertIsNone(self.profile.deletion_notice_date) + + def test_it_checks_team_members(self): + # bob has access to alice's project + Member.objects.create(user=self.bob, project=self.project) + + cmd = Command() + cmd.stdout = Mock() # silence output to stdout + + result = cmd.handle() + self.assertEqual(result, "Done! Sent 0 notices") + + self.profile.refresh_from_db() + self.assertIsNone(self.profile.deletion_notice_date) + + def test_it_checks_recent_pings(self): + check = Check.objects.create(project=self.project) + Ping.objects.create(owner=check) + + cmd = Command() + cmd.stdout = Mock() # silence output to stdout + + result = cmd.handle() + self.assertEqual(result, "Done! Sent 0 notices") + + self.profile.refresh_from_db() + self.assertIsNone(self.profile.deletion_notice_date) + + def test_it_checks_last_active_date(self): + # alice has been browsing the site recently + self.profile.last_active_date = now() - td(days=15) + self.profile.save() + + cmd = Command() + cmd.stdout = Mock() # silence output to stdout + + result = cmd.handle() + self.assertEqual(result, "Done! Sent 0 notices") diff --git a/hc/api/management/commands/sendreports.py b/hc/api/management/commands/sendreports.py index 0214742f..3e543ee7 100644 --- a/hc/api/management/commands/sendreports.py +++ b/hc/api/management/commands/sendreports.py @@ -1,4 +1,3 @@ -from datetime import timedelta import time from django.core.management.base import BaseCommand