mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-07 06:05:34 +00:00
Management command for sending inactive account notifications
This commit is contained in:
parent
acd55ce7f3
commit
945a66ab0a
12 changed files with 161 additions and 5 deletions
CHANGELOG.md
hc
templates/emails
|
@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
|
||||||
- Add maxlength attribute to HTML input=text elements
|
- Add maxlength attribute to HTML input=text elements
|
||||||
- Improved logic for displaying job execution times in log (#219)
|
- Improved logic for displaying job execution times in log (#219)
|
||||||
- Add Matrix integration
|
- Add Matrix integration
|
||||||
|
- Add a management command for sending inactive account notifications
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
- Fix refreshing of the checks page filtered by tags (#221)
|
- Fix refreshing of the checks page filtered by tags (#221)
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ProfileFieldset(Fieldset):
|
||||||
name = "User Profile"
|
name = "User Profile"
|
||||||
fields = ("email", "current_project", "reports_allowed",
|
fields = ("email", "current_project", "reports_allowed",
|
||||||
"next_report_date", "nag_period", "next_nag_date",
|
"next_report_date", "nag_period", "next_nag_date",
|
||||||
|
"deletion_notice_date",
|
||||||
"token", "sort")
|
"token", "sort")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,9 @@ from datetime import timedelta
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import Count
|
from django.db.models import Count, F
|
||||||
from django.utils import timezone
|
from django.utils.timezone import now
|
||||||
|
from hc.accounts.models import Profile
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -18,12 +19,25 @@ class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
cutoff = timezone.now() - timedelta(days=30)
|
month_ago = now() - timedelta(days=30)
|
||||||
|
|
||||||
# Old accounts, never logged in, no team memberships
|
# Old accounts, never logged in, no team memberships
|
||||||
q = User.objects.order_by("id")
|
q = User.objects.order_by("id")
|
||||||
q = q.annotate(n_teams=Count("memberships"))
|
q = q.annotate(n_teams=Count("memberships"))
|
||||||
q = q.filter(date_joined__lt=cutoff, last_login=None, n_teams=0)
|
q = q.filter(date_joined__lt=month_ago, last_login=None, n_teams=0)
|
||||||
|
|
||||||
n, summary = q.delete()
|
n, summary = q.delete()
|
||||||
return "Done! Pruned %d user accounts." % summary.get("auth.User", 0)
|
count = summary.get("auth.User", 0)
|
||||||
|
self.stdout.write("Pruned %d never-logged-in user accounts." % count)
|
||||||
|
|
||||||
|
# Profiles scheduled for deletion
|
||||||
|
q = Profile.objects.order_by("id")
|
||||||
|
q = q.filter(deletion_notice_date__lt=month_ago)
|
||||||
|
# Exclude users who have logged in after receiving deletion notice
|
||||||
|
q = q.exclude(user__last_login__gt=F("deletion_notice_date"))
|
||||||
|
|
||||||
|
for profile in q:
|
||||||
|
self.stdout.write("Deleting inactive %s" % profile.user.email)
|
||||||
|
profile.user.delete()
|
||||||
|
|
||||||
|
return "Done!"
|
||||||
|
|
63
hc/accounts/management/commands/senddeletionnotices.py
Normal file
63
hc/accounts/management/commands/senddeletionnotices.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from hc.accounts.models import Profile, Member
|
||||||
|
from hc.api.models import Ping
|
||||||
|
from hc.lib import emails
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = """Send deletion notices to inactive user accounts.
|
||||||
|
|
||||||
|
Conditions for sending the notice:
|
||||||
|
- deletion notice has not been sent recently
|
||||||
|
- last login more than a year ago
|
||||||
|
- none of the owned projects has invited team members
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
q = q.exclude(user__last_login__gt=year_ago)
|
||||||
|
# Exclude accounts less than a year_ago 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 paid accounts
|
||||||
|
q = q.exclude(sms_limit__gt=0)
|
||||||
|
|
||||||
|
sent = 0
|
||||||
|
for profile in q:
|
||||||
|
members = Member.objects.filter(project__owner_id=profile.user_id)
|
||||||
|
if members.exists():
|
||||||
|
print("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)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.stdout.write("Sending notice to %s" % profile.user.email)
|
||||||
|
|
||||||
|
profile.deletion_notice_date = now()
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
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)
|
||||||
|
sent += 1
|
||||||
|
|
||||||
|
return "Done! Sent %d notices" % sent
|
18
hc/accounts/migrations/0027_profile_deletion_notice_date.py
Normal file
18
hc/accounts/migrations/0027_profile_deletion_notice_date.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.1.7 on 2019-03-12 17:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0026_auto_20190204_2042'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='deletion_notice_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -56,6 +56,7 @@ class Profile(models.Model):
|
||||||
sms_sent = models.IntegerField(default=0)
|
sms_sent = models.IntegerField(default=0)
|
||||||
team_limit = models.IntegerField(default=2)
|
team_limit = models.IntegerField(default=2)
|
||||||
sort = models.CharField(max_length=20, default="created")
|
sort = models.CharField(max_length=20, default="created")
|
||||||
|
deletion_notice_date = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
objects = ProfileManager()
|
objects = ProfileManager()
|
||||||
|
|
||||||
|
|
18
hc/api/migrations/0058_auto_20190312_1716.py
Normal file
18
hc/api/migrations/0058_auto_20190312_1716.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.1.7 on 2019-03-12 17:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0057_auto_20190118_1319'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='channel',
|
||||||
|
name='kind',
|
||||||
|
field=models.CharField(choices=[('email', 'Email'), ('webhook', 'Webhook'), ('hipchat', 'HipChat'), ('slack', 'Slack'), ('pd', 'PagerDuty'), ('pagertree', 'PagerTree'), ('po', 'Pushover'), ('pushbullet', 'Pushbullet'), ('opsgenie', 'OpsGenie'), ('victorops', 'VictorOps'), ('discord', 'Discord'), ('telegram', 'Telegram'), ('sms', 'SMS'), ('zendesk', 'Zendesk'), ('trello', 'Trello'), ('matrix', 'Matrix')], max_length=20),
|
||||||
|
),
|
||||||
|
]
|
|
@ -70,3 +70,7 @@ def invoice(to, ctx, filename, pdf_data):
|
||||||
msg.attach_alternative(html, "text/html")
|
msg.attach_alternative(html, "text/html")
|
||||||
msg.attach(filename, pdf_data, "application/pdf")
|
msg.attach(filename, pdf_data, "application/pdf")
|
||||||
msg.send()
|
msg.send()
|
||||||
|
|
||||||
|
|
||||||
|
def deletion_notice(to, ctx, headers={}):
|
||||||
|
send("deletion-notice", to, ctx, headers)
|
||||||
|
|
|
@ -31,6 +31,7 @@ SECRET_KEY = os.getenv("SECRET_KEY", "---")
|
||||||
DEBUG = envbool("DEBUG", "True")
|
DEBUG = envbool("DEBUG", "True")
|
||||||
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",")
|
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",")
|
||||||
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "healthchecks@example.org")
|
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "healthchecks@example.org")
|
||||||
|
SUPPORT_EMAIL = os.getenv("SUPPORT_EMAIL")
|
||||||
USE_PAYMENTS = envbool("USE_PAYMENTS", "False")
|
USE_PAYMENTS = envbool("USE_PAYMENTS", "False")
|
||||||
REGISTRATION_OPEN = envbool("REGISTRATION_OPEN", "True")
|
REGISTRATION_OPEN = envbool("REGISTRATION_OPEN", "True")
|
||||||
|
|
||||||
|
|
20
templates/emails/deletion-notice-body-html.html
Normal file
20
templates/emails/deletion-notice-body-html.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "emails/base.html" %}
|
||||||
|
{% load hc_extras %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
Hello,<br />
|
||||||
|
|
||||||
|
We’re sending this email to notify you that your {% site_name %} account, registered to {{ email }} has been inactive for 1 year or more. If you no longer wish to keep your {% site_name %} account active then we will make sure that your account is closed and any data associated with your account is permanently deleted from our systems.<br /><br />
|
||||||
|
|
||||||
|
If you wish to keep your account, simply log in within 30 days. If you continue to be inactive, <strong>your account will be permanently deleted after the 30 day period</strong>.<br /><br />
|
||||||
|
|
||||||
|
If you have issues logging in, or have any questions, please reach out to us at {{ support_email }}.<br /><br />
|
||||||
|
|
||||||
|
Sincerely,<br />
|
||||||
|
The {% site_name %} Team
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block unsub %}
|
||||||
|
<br />
|
||||||
|
This is a one-time message we're sending out to notify you about your account closure.
|
||||||
|
{% endblock %}
|
14
templates/emails/deletion-notice-body-text.html
Normal file
14
templates/emails/deletion-notice-body-text.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% load hc_extras %}
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
We’re sending this email to notify you that your {% site_name %} account, registered to {{ email }} has been inactive for 1 year or more. If you no longer wish to keep your {% site_name %} account active then we will make sure that your account is closed and any data associated with your account is permanently deleted from our systems.
|
||||||
|
|
||||||
|
If you wish to keep your account, simply log in within 30 days. If you continue to be inactive, your account will be permanently deleted after the 30 day period.
|
||||||
|
|
||||||
|
If you have issues logging in, or have any questions, please reach out to us at {{ support_email }}.
|
||||||
|
|
||||||
|
This is a one-time message we're sending out to notify you about your account closure.
|
||||||
|
|
||||||
|
--
|
||||||
|
Sincerely,
|
||||||
|
The {% site_name %} Team
|
1
templates/emails/deletion-notice-subject.html
Normal file
1
templates/emails/deletion-notice-subject.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Inactive Account Notification
|
Loading…
Add table
Reference in a new issue