management: add new command clean-unused-accounts

This command takes one required argument the number of days before
deleting an account. Accounts not logged since this number of days are
sent an email using templates,
authentic2/unused_account_delete_subject.txt and
authentic2/unused_account_delete_body.txt and are deleted using the
DeletedUser model, to allow for mass deletion and actions on deletion.
The template receives two variable: user and the days threshold.

The --alert-thresholds parameter allow to set threshold in days after
which accounts will receive an alert email warning people of the future
deletion of their account. Alert thresholds are given as a comma
separated list of days count, each days count must be inferior to the
delete threshold. The mail templates are
authentic2/unused_account_alert_subject.txt and
authentic2/unused_account_alert_body.txt. The template receives three
variable: user, the current alert threshold and the remaining days
before reaching the delete threshold.

You can limit cleaning to only some kind of accounts using the --filter
option, for example --filter groups__name="Online registration" will
limit the cleaning to accounts in the "Online registration" group.

The --fake option will only print actions done and will not send emails
or delete accounts.

The --period option is the number of days between two runs of the
clean-unused-accounts command, it defaults to one day.
This commit is contained in:
Benjamin Dauvergne 2014-07-08 14:38:10 +02:00
parent 02f3a2bae8
commit ed76842bd5
6 changed files with 141 additions and 2 deletions

View File

@ -0,0 +1,130 @@
from optparse import make_option
import logging
import datetime
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
from django.core.mail import send_mail
from django.utils.timezone import now
from django.template.loader import render_to_string
from authentic2.models import DeletedUser
from django.conf import settings
def print_table(table):
col_width = [max(len(x) for x in col) for col in zip(*table)]
for line in table:
line = u"| " + u" | ".join(u"{0:>{1}}".format(x, col_width[i])
for i, x in enumerate(line)) + u" |"
print line
class Command(BaseCommand):
args = '<clean_threshold>'
help = '''Clean unused accounts'''
option_list = BaseCommand.option_list + (
make_option("--alert-thresholds",
help='list of durations before sending an alert '
'message for unused account, default is none',
default = None),
make_option("--period", type='int',
help='period between two calls to '
'clean-unused-accounts as days, default is 1',
default=1),
make_option("--fake", action='store_true', help='do nothing',
default=False),
make_option("--filter", help='filter to apply to the user queryset, '
'the Django filter key and value are separated by character =', action='append', default=[]),
make_option('--from-email', default=settings.DEFAULT_FROM_EMAIL,
help='sender address for notifications, default is DEFAULT_FROM_EMAIL from settings'),
)
def handle(self, *args, **options):
if len(args) < 1:
raise CommandError('missing clean_threshold')
if options['period'] < 1:
raise CommandError('period must be > 0')
try:
clean_threshold = int(args[0])
if clean_threshold < 1:
raise ValueError()
except ValueError:
raise CommandError('clean_threshold must be an integer > 0')
if options['verbosity'] == '0':
logging.basicConfig(level=logging.CRITICAL)
if options['verbosity'] == '1':
logging.basicConfig(level=logging.WARNING)
elif options['verbosity'] == '2':
logging.basicConfig(level=logging.INFO)
elif options['verbosity'] == '3':
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)
n = now().replace(hour=0, minute=0, second=0, microsecond=0)
self.fake = options['fake']
self.from_email = options['from_email']
if self.fake:
log.info('fake call to clean-unused-accounts')
users = User.objects.all()
if options['filter']:
for f in options['filter']:
key, value = f.split('=', 1)
try:
users = users.filter(**{key: value})
except:
raise CommandError('invalid --filter %s' % f)
if options['alert_thresholds']:
alert_thresholds = options['alert_thresholds']
alert_thresholds = alert_thresholds.split(',')
try:
alert_thresholds = map(int, alert_thresholds)
except ValueError:
raise CommandError('alert_thresholds must be a comma '
'separated list of integers')
for threshold in alert_thresholds:
if not (0 < threshold < clean_threshold):
raise CommandError('alert-threshold must a positive integer '
'inferior to clean-threshold: 0 < %d < %d' % (
threshold, clean_threshold))
for threshold in alert_thresholds:
a = n - datetime.timedelta(days=threshold)
b = n - datetime.timedelta(days=threshold-options['period'])
for user in users.filter(last_login__lt=b, last_login__gte=a):
log.info('%s last login %d days ago, sending alert', user, threshold)
self.send_alert(user, threshold, clean_threshold-threshold)
threshold = n - datetime.timedelta(days=clean_threshold)
for user in users.filter(last_login__lt=threshold):
d = n - user.last_login
log.info('%s last login %d days ago, deleting user', user, d.days)
self.delete_user(user, clean_threshold)
def send_alert(self, user, threshold, clean_threshold):
ctx = { 'user': user, 'threshold': threshold,
'clean_threshold': clean_threshold }
self.send_mail('authentic2/unused_account_alert', user, ctx)
def send_mail(self, prefix, user, ctx):
log = logging.getLogger(__name__)
if not user.email:
log.debug('%s has no email, no mail sent', user)
subject = render_to_string(prefix + '_subject.txt', ctx).strip()
body = render_to_string(prefix + '_body.txt', ctx)
if not self.fake:
try:
log.debug('sending mail to %s', user.email)
send_mail(subject, body, self.from_email, [user.email])
except:
log.exception('email sending failure')
def delete_user(self, user, threshold):
ctx = { 'user': user, 'threshold': threshold }
self.send_mail('authentic2/unused_account_delete', user,
ctx)
if not self.fake:
DeletedUser.objects.delete_user(user)

View File

@ -33,9 +33,9 @@ class DeletedUserManager(models.Manager):
user.save()
self.create(user=user)
def cleanup(self):
def cleanup(self, threshold=600):
'''Delete all deleted users for more than 10 minutes.'''
not_after = now() - timedelta(seconds=600)
not_after = now() - timedelta(seconds=threshold)
for deleted_user in self.filter(creation__lte=not_after):
user = deleted_user.user
deleted_user.delete()

View File

@ -0,0 +1,4 @@
{% load i18n %}{% blocktrans %}Hi {{ user }},
You have not logged since {{ threshold }} days. In {{ clean_threshold }} days your account
will be deleted.{% endblocktrans %}

View File

@ -0,0 +1 @@
{% load i18n %}{% blocktrans %}Alert: {{ user }} you have not logged since {{ threshold }}{% endblocktrans %}

View File

@ -0,0 +1,3 @@
{% load i18n %}{% blocktrans %}Hi {{ user }},
You have not logged since {{ threshold }} days so your account has been deleted.{% endblocktrans %}

View File

@ -0,0 +1 @@
{% load i18n %}{% blocktrans %}Notification: {{ user }}, your account has been deleted{% endblocktrans %}