Browse Source

Merge pull request #1045 from rafalp/remove-inactive-users

Add deleteinactiveusers management command
Rafał Pitoń 6 years ago
parent
commit
c86190b590

+ 8 - 0
misago/conf/defaults.py

@@ -52,6 +52,14 @@ MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS = 48
 MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR = None
 
 
+# Automatically delete new user accounts that weren't activated in specified time
+# If you rely on admin review of new registrations, make this period long, disable
+# the "deleteinactiveusers" management command, or change this value to zero. Otherwise
+# keep it short to give users a chance to retry on their own after few days pass.s
+
+MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS = 0
+
+
 # Allow users to delete their accounts
 # Lets users delete their own account on the site without having to contact site administrators.
 # This mechanism doesn't delete user posts, polls or attachments, but attempts to anonymize any

+ 1 - 0
misago/project_template/cron.txt

@@ -11,5 +11,6 @@
 25 0 * * * python manage.py invalidatebans
 0 2 * * * python manage.py removeoldips
 0 1 * * * python manage.py deletemarkedusers
+25 1 * * * python manage.py deleteinactiveusers
 0 2 * * * python manage.py expireuserdatadownloads
 0 7 * * * python manage.py prepareuserdatadownloads

+ 29 - 21
misago/project_template/project_name/settings.py

@@ -156,26 +156,6 @@ EMAIL_HOST_PASSWORD = ''
 DEFAULT_FROM_EMAIL = 'Forums <%s>' % EMAIL_HOST_USER
 
 
-# Allow users to download their personal data
-# Enables users to learn what data about them is being held by the site without having to contact
-# site's administrators.
-
-MISAGO_ENABLE_DOWNLOAD_OWN_DATA = True
-
-# Path to the directory that Misago should use to prepare user data downloads.
-# Should not be accessible from internet.
-
-MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR = os.path.join(BASE_DIR, 'userdata')
-
-
-# Allow users to delete their accounts
-# Lets users delete their own account on the site without having to contact site administrators.
-# This mechanism doesn't delete user posts, polls or attachments, but attempts to anonymize any
-# data about user left behind after user is deleted.
-
-MISAGO_ENABLE_DELETE_OWN_ACCOUNT = True
-
-
 # Application definition
 
 AUTH_USER_MODEL = 'misago_users.User'
@@ -388,7 +368,7 @@ MISAGO_ADDRESS = 'http://my-misago-site.com/'
 
 
 # PostgreSQL text search configuration to use in searches
-# Defaults to "simple", for list of installed configurations run "\dF" in "psql"
+# Defaults to "simple", for list of installed configurations run "\dF" in "psql".
 # Standard configs as of PostgreSQL 9.5 are: dutch, english, finnish, french,
 # german, hungarian, italian, norwegian, portuguese, romanian, russian, simple,
 # spanish, swedish and turkish
@@ -397,6 +377,34 @@ MISAGO_ADDRESS = 'http://my-misago-site.com/'
 MISAGO_SEARCH_CONFIG = 'simple'
 
 
+# Allow users to download their personal data
+# Enables users to learn what data about them is being held by the site without having to contact
+# site's administrators.
+
+MISAGO_ENABLE_DOWNLOAD_OWN_DATA = True
+
+# Path to the directory that Misago should use to prepare user data downloads.
+# Should not be accessible from internet.
+
+MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR = os.path.join(BASE_DIR, 'userdata')
+
+
+# Allow users to delete their accounts
+# Lets users delete their own account on the site without having to contact site administrators.
+# This mechanism doesn't delete user posts, polls or attachments, but attempts to anonymize any
+# data about user left behind after user is deleted.
+
+MISAGO_ENABLE_DELETE_OWN_ACCOUNT = True
+
+
+# Automatically delete new user accounts that weren't activated in specified time
+# If you rely on admin review of new registrations, make this period long, disable
+# the "deleteinactiveusers" management command, or change this value to zero. Otherwise
+# keep it short to give users a chance to retry on their own after few days pass.
+
+MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS = 2
+
+
 # Path to directory containing avatar galleries
 # Those galleries can be loaded by running loadavatargallery command
 

+ 41 - 0
misago/users/management/commands/deleteinactiveusers.py

@@ -0,0 +1,41 @@
+from __future__ import unicode_literals
+
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+
+from misago.conf import settings
+from misago.core.pgutils import chunk_queryset
+
+
+UserModel = get_user_model()
+
+
+class Command(BaseCommand):
+    help = (
+        "Deletes inactive user accounts older than set time."
+    )
+
+    def handle(self, *args, **options):
+        if not settings.MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS:
+            self.stdout.write("Automatic deletion of inactive users is currently disabled.")
+            return
+
+
+        users_deleted = 0
+        
+        joined_on_cutoff = timezone.now() - timedelta(
+            days=settings.MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS)
+
+        queryset = UserModel.objects.filter(
+            requires_activation__gt=UserModel.ACTIVATION_NONE,
+            joined_on__lt=joined_on_cutoff,
+        )
+
+        for user in chunk_queryset(queryset):
+            user.delete()
+            users_deleted += 1
+
+        self.stdout.write("Deleted users: {}".format(users_deleted))

+ 109 - 0
misago/users/tests/test_deleteinactiveusers.py

@@ -0,0 +1,109 @@
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.core.management import call_command
+from django.test import TestCase, override_settings
+from django.utils import timezone
+from django.utils.six import StringIO
+
+from misago.users.management.commands import deleteinactiveusers
+
+
+UserModel = get_user_model()
+
+
+class DeleteInactiveUsersTests(TestCase):
+    def setUp(self):
+        self.user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123')
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_delete_user_activation_user(self):
+        """deletes user that didn't activate their account within required time"""
+        self.user.joined_on = timezone.now() - timedelta(days=2)
+        self.user.requires_activation = UserModel.ACTIVATION_USER
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 1")
+
+        with self.assertRaises(UserModel.DoesNotExist):
+            UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_delete_user_activation_admin(self):
+        """deletes user that wasn't activated by admin within required time"""
+        self.user.joined_on = timezone.now() - timedelta(days=2)
+        self.user.requires_activation = UserModel.ACTIVATION_ADMIN
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 1")
+
+        with self.assertRaises(UserModel.DoesNotExist):
+            UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_skip_new_user_activation_user(self):
+        """skips inactive user that is too new"""
+        self.user.joined_on = timezone.now() - timedelta(days=1)
+        self.user.requires_activation = UserModel.ACTIVATION_USER
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 0")
+
+        UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_skip_new_user_activation_admin(self):
+        """skips admin-activated user that is too new"""
+        self.user.joined_on = timezone.now() - timedelta(days=1)
+        self.user.requires_activation = UserModel.ACTIVATION_ADMIN
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 0")
+
+        UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_skip_active_user(self):
+        """skips active user"""
+        self.user.joined_on = timezone.now() - timedelta(days=1)
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 0")
+
+        UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=0)
+    def test_delete_inactive_is_disabled(self):
+        """skips active user"""
+        self.user.joined_on = timezone.now() - timedelta(days=1)
+        self.user.requires_activation = UserModel.ACTIVATION_ADMIN
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(
+            command_output, "Automatic deletion of inactive users is currently disabled.")
+
+        UserModel.objects.get(pk=self.user.pk)