Skip to content

Commit

Permalink
Merge pull request #2 from incuna/add-command-notify-user
Browse files Browse the repository at this point in the history
Add management command to notify and delete users
  • Loading branch information
adam-thomas committed Apr 28, 2016
2 parents db37173 + 9ef4d62 commit 645244e
Show file tree
Hide file tree
Showing 17 changed files with 312 additions and 1 deletion.
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ colour-runner==0.0.4
coverage==4.0.3
dj-database-url==0.4.1
django==1.8.12
factory-boy==2.6.1
flake8==2.5.4
flake8-import-order==0.7
incuna-pigeon==0.1.0
python-dateutil==2.5.2
12 changes: 12 additions & 0 deletions tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import factory

from .models import User


class UserFactory(factory.DjangoModelFactory):
name = factory.Sequence('User {}'.format)
username = factory.Sequence('username{}'.format)
email = factory.Sequence('email{}@incuna.com'.format)

class Meta:
model = User
18 changes: 18 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.contrib.auth.models import AbstractUser
from django.db import models

from user_deletion.managers import UserDeletionManagerMixin


class UserManager(UserDeletionManagerMixin, models.Manager):
pass


class User(AbstractUser):
name = models.CharField(max_length=255)
notified = models.BooleanField(default=False)

objects = UserManager()

def __str__(self):
return self.name
13 changes: 12 additions & 1 deletion tests/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,19 @@
default='sqlite://{}/user_deletion.db'.format(BASEDIR),
),
},
INSTALLED_APPS=('user_deletion',),
INSTALLED_APPS=(
'tests',
'user_deletion',

'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
),
MIDDLEWARE_CLASSES=(),

AUTH_USER_MODEL='tests.User',
SITE_ID=1,
DEFAULT_FROM_EMAIL='[email protected]',
)


Expand Down
54 changes: 54 additions & 0 deletions tests/test_management_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from unittest.mock import patch

from dateutil.relativedelta import relativedelta
from django.core import mail
from django.core.management import call_command
from django.test import TestCase
from django.utils import timezone

from .factories import UserFactory
from .models import User


MONTH = 1


@patch('user_deletion.apps.UserDeletionConfig.MONTH_NOTIFICATION', new=MONTH)
class TestUserNotifyManagementCommand(TestCase):
def test_no_users(self):
call_command('notify_users')

self.assertEqual(len(mail.outbox), 0)

def test_inactive_users(self):
month_ago = timezone.now() - relativedelta(months=MONTH)
UserFactory.create(last_login=month_ago)

call_command('notify_users')

self.assertEqual(len(mail.outbox), 1)

def test_notified_users(self):
year_ago = timezone.now() - relativedelta(months=MONTH)
UserFactory.create(last_login=year_ago, notified=True)

call_command('notify_users')

self.assertEqual(len(mail.outbox), 0)


@patch('user_deletion.apps.UserDeletionConfig.MONTH_DELETION', new=MONTH)
class TestDeleteUsersManagementCommand(TestCase):
def test_no_users(self):
call_command('delete_users')
self.assertEqual(len(mail.outbox), 0)

def test_inactive_users(self):
year_ago = timezone.now() - relativedelta(months=MONTH)
user = UserFactory.create(last_login=year_ago, notified=True)

call_command('delete_users')

self.assertEqual(len(mail.outbox), 1)
with self.assertRaises(User.DoesNotExist):
user.refresh_from_db()
60 changes: 60 additions & 0 deletions tests/test_managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from dateutil.relativedelta import relativedelta
from django.apps import apps
from django.test import TestCase
from django.utils import timezone

from .factories import UserFactory
from .models import User


user_deletion_config = apps.get_app_config('user_deletion')


class TestUserDeletionManager(TestCase):
def test_users_to_notify(self):
last_login = timezone.now() - relativedelta(
months=user_deletion_config.MONTH_NOTIFICATION,
)
user = UserFactory.create(last_login=last_login, notified=False)
users = User.objects.users_to_notify()

self.assertCountEqual(users, [user])

def test_users_not_to_notify(self):
user = UserFactory.create(last_login=timezone.now(), notified=False)
users = User.objects.users_to_notify()

self.assertNotIn(user, users)

def test_users_already_notified(self):
last_login = timezone.now() - relativedelta(
months=user_deletion_config.MONTH_NOTIFICATION,
)
user = UserFactory.create(last_login=last_login, notified=True)
users = User.objects.users_to_notify()

self.assertNotIn(user, users)

def test_users_to_delete(self):
last_login = timezone.now() - relativedelta(
months=user_deletion_config.MONTH_DELETION,
)
user = UserFactory.create(last_login=last_login, notified=True)
users = User.objects.users_to_delete()

self.assertCountEqual(users, [user])

def test_users_not_to_delete(self):
user = UserFactory.create(last_login=timezone.now(), notified=False)
users = User.objects.users_to_delete()

self.assertNotIn(user, users)

def test_users_to_delete_not_notified(self):
last_login = timezone.now() - relativedelta(
months=user_deletion_config.MONTH_DELETION,
)
user = UserFactory.create(last_login=last_login, notified=False)
users = User.objects.users_to_delete()

self.assertNotIn(user, users)
33 changes: 33 additions & 0 deletions tests/test_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.contrib.sites.models import Site
from django.core import mail
from django.test import TestCase

from user_deletion.notifications import (
AccountDeletedNotification,
AccountInactiveNotification,
)
from .factories import UserFactory


class TestAccountDeletedNotification(TestCase):
def test_notification(self):
user = UserFactory.create()
site = Site.objects.get_current()

AccountDeletedNotification(user=None, site=site, users=[user]).notify()

email = mail.outbox[0]
self.assertEqual(email.subject, 'Your account has been deleted')
self.assertIn('You were informed', email.body)


class TestAccountInactiveNotification(TestCase):
def test_notification(self):
user = UserFactory.create()
site = Site.objects.get_current()

AccountInactiveNotification(user=None, site=site, users=[user]).notify()

email = mail.outbox[0]
self.assertEqual(email.subject, 'Re-activate your account')
self.assertIn('We have noticed', email.body)
1 change: 1 addition & 0 deletions user_deletion/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'user_deletion.apps.UserDeletionConfig'
10 changes: 10 additions & 0 deletions user_deletion/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.apps import AppConfig


class UserDeletionConfig(AppConfig):
name = 'user_deletion'

# users are notificed after 12 months of inactivity
MONTH_NOTIFICATION = 12
# users are deleted after 13 months
MONTH_DELETION = 13
Empty file.
18 changes: 18 additions & 0 deletions user_deletion/management/commands/delete_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand
from django.utils import translation

from ...notifications import AccountDeletedNotification

User = get_user_model()


class Command(BaseCommand):
def handle(self, *args, **options):
translation.activate(settings.LANGUAGE_CODE)
users = User.objects.users_to_delete()
site = Site.objects.get_current()
AccountDeletedNotification(user=None, site=site, users=users).notify()
users.delete()
17 changes: 17 additions & 0 deletions user_deletion/management/commands/notify_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand
from django.utils import translation

from ...notifications import AccountInactiveNotification

User = get_user_model()


class Command(BaseCommand):
def handle(self, *args, **options):
translation.activate(settings.LANGUAGE_CODE)
users = User.objects.users_to_notify()
site = Site.objects.get_current()
AccountInactiveNotification(user=None, site=site, users=users).notify()
22 changes: 22 additions & 0 deletions user_deletion/managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from dateutil.relativedelta import relativedelta
from django.apps import apps
from django.utils import timezone


user_deletion_config = apps.get_app_config('user_deletion')


class UserDeletionManagerMixin:
def users_to_notify(self):
"""Finds all users who have been inactive and not yet notified."""
threshold = timezone.now() - relativedelta(
months=user_deletion_config.MONTH_NOTIFICATION,
)
return self.filter(last_login__lte=threshold, notified=False)

def users_to_delete(self):
"""Finds all users who have been inactive and were notified."""
threshold = timezone.now() - relativedelta(
months=user_deletion_config.MONTH_DELETION,
)
return self.filter(last_login__lte=threshold, notified=True)
34 changes: 34 additions & 0 deletions user_deletion/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.conf import settings
from django.core.mail import send_mass_mail
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from pigeon.notification import Notification


def build_emails(notification):
context = {'site': notification.site}
for user in notification.users:
message = render_to_string(notification.template_name, context)
yield [
notification.subject,
message,
settings.DEFAULT_FROM_EMAIL,
[user.email],
]


def send_emails(notification):
messages = build_emails(notification)
send_mass_mail(messages)


class AccountInactiveNotification(Notification):
handlers = (send_emails,)
template_name = 'user_deletion/email_notification.txt'
subject = _('Re-activate your account')


class AccountDeletedNotification(Notification):
handlers = (send_emails,)
template_name = 'user_deletion/email_deletion.txt'
subject = _('Your account has been deleted')
1 change: 1 addition & 0 deletions user_deletion/templates/user_deletion/base.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% block content %}{% endblock content %}
10 changes: 10 additions & 0 deletions user_deletion/templates/user_deletion/email_deletion.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% load i18n %}
{% block content %}{% blocktrans with site_domain=site.domain %}
Your account at {{ site_domain }} has been now deleted.

You were informed about the fact that your account was inactive for over 12 months and
it was due to be deleted if no action will be taken.

We have now deleted your profile with its all data associated, as no required
action was taken.
{% endblocktrans %}{% endblock content %}
7 changes: 7 additions & 0 deletions user_deletion/templates/user_deletion/email_notification.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% load i18n %}
{% block content %}{% blocktrans with site_domain=site.domaine %}
We have noticed that your account at {{ site_domain }} has been inactive for over 12 months now.

Please log in to your account in order to keep it alive, otherwise it will be
deleted within the next four weeks.
{% endblocktrans %}{% endblock content %}

0 comments on commit 645244e

Please sign in to comment.