core: Added management command to clean inactive accounts.

see #1196
This commit is contained in:
Antoine Nguyen
2017-07-28 10:06:29 +02:00
parent e6ed1adba4
commit c613ad4572
5 changed files with 128 additions and 9 deletions
+10
View File
@@ -271,6 +271,16 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous"))
inactive_account_threshold = forms.IntegerField(
label=_("Inactive account threshold"),
initial=30,
help_text=_(
"An account with a last login date greater than this threshold "
"(in days) will be considered as inactive"
),
widget=forms.TextInput(attrs={"class": "form-control"})
)
top_notifications_check_interval = forms.IntegerField(
label=_("Top notifications check interval"),
initial=30,
+10 -7
View File
@@ -92,14 +92,17 @@ def create_local_config(sender, **kwargs):
@receiver(signals.pre_delete, sender=models.User)
def update_permissions(sender, instance, **kwargs):
"""Permissions cleanup."""
from_user = get_request().user
if from_user == instance:
raise exceptions.PermDeniedException(
_("You can't delete your own account")
)
request = get_request()
# request migth be None (management command context)
if request:
from_user = request.user
if from_user == instance:
raise exceptions.PermDeniedException(
_("You can't delete your own account")
)
if not from_user.can_access(instance):
raise exceptions.PermDeniedException
if not from_user.can_access(instance):
raise exceptions.PermDeniedException
# We send an additional signal before permissions are removed
core_signals.account_deleted.send(
@@ -0,0 +1,71 @@
"""Inactive accounts cleanup tool."""
from __future__ import print_function, unicode_literals
from dateutil.relativedelta import relativedelta
from six.moves import input
from django.core.management.base import BaseCommand
from django.utils import timezone
from modoboa.parameters import tools as param_tools
from ... import models
class Command(BaseCommand):
"""Management command to clean inactive accounts."""
help = "Inactive accounts cleanup"
def add_arguments(self, parser):
"""Add extra arguments to command line."""
parser.add_argument(
"--delete", action="store_true", default=False,
help="Delete inactive accounts (default is to disable)")
parser.add_argument(
"--dry-run", action="store_true", default=False,
help="Only look for inactive accounts, no action")
parser.add_argument(
"--silent", action="store_true", default=False,
help="Enable silent mode (no question)")
parser.add_argument(
"--verbose", action="store_true", default=False,
help="Enable verbose mode")
def _log_inactive_accounts(self, qset):
"""Log inactive accounts found."""
print(
"The following inactive accounts have been found:",
file=self.stdout)
for account in qset.values("username", "last_login"):
print(
"> {}\n (last login: {})".format(
account["username"], account["last_login"]),
file=self.stdout
)
def handle(self, *args, **options):
"""Command entry point."""
inactive_account_threshold = param_tools.get_global_parameter(
"inactive_account_threshold")
qset = models.User.objects.filter(
is_active=True, is_superuser=False,
last_login__lt=timezone.now() -
relativedelta(days=inactive_account_threshold))
if not qset.exists():
return
if options["verbose"]:
self._log_inactive_accounts(qset)
if options["dry_run"]:
return
action = "delete" if options["delete"] else "disable"
if not options["silent"]:
answer = input(
"Do you want to {} those accounts? (y/N) ".format(action))
if not answer.lower().startswith("y"):
return
if action == "disable":
qset.update(is_active=False)
else:
qset.delete()
+35
View File
@@ -5,10 +5,12 @@ from __future__ import unicode_literals
import httmock
from dateutil.relativedelta import relativedelta
from six import StringIO
from django.core import management
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils import timezone
from modoboa.lib.tests import ModoTestCase
@@ -72,6 +74,39 @@ class ManagementCommandsTestCase(TestCase):
management.call_command("cleanlogs")
self.assertEqual(models.Log.objects.count(), 1)
def test_clean_inactive_accounts(self):
"""Run clean_inactive_accounts command."""
management.call_command("load_initial_data")
# no inactive account, should exit normaly
management.call_command("clean_inactive_accounts")
last_login = timezone.now() - relativedelta(days=45)
account = factories.UserFactory(
username="user1@domain.test", groups=("SimpleUsers", ),
last_login=last_login
)
management.call_command("clean_inactive_accounts", "--dry-run")
account.refresh_from_db()
self.assertTrue(account.is_active)
out = StringIO()
management.call_command(
"clean_inactive_accounts", "--verbose", "--dry-run", stdout=out)
self.assertIn("user1@domain.test", out.getvalue())
management.call_command("clean_inactive_accounts", "--silent")
account.refresh_from_db()
self.assertFalse(account.is_active)
account.is_active = True
account.save(update_fields=["is_active"])
management.call_command(
"clean_inactive_accounts", "--silent", "--delete")
with self.assertRaises(models.User.DoesNotExist):
account.refresh_from_db()
class ProfileTestCase(ModoTestCase):
"""Profile related tests."""
+2 -2
View File
@@ -118,9 +118,9 @@ def ungrant_access_to_object(obj, user=None):
def ungrant_access_to_objects(objects):
"""Cancel all accesses for a given objects list
"""Cancel all accesses for a given object list.
:param objects: a list of objects inheriting from ``model.Model``
:param objects: a list of objects inheriting from ``models.Model``
"""
for obj in objects:
ct = ContentType.objects.get_for_model(obj)