From b23581a5aef09d69b03ba57251594f0a0ec57a6a Mon Sep 17 00:00:00 2001
From: Serghei Mihai <smihai@entrouvert.com>
Date: Tue, 9 Feb 2021 10:42:52 +0100
Subject: [PATCH] custom_user: add field to store user deactivation time
 (#50966)

---
 src/authentic2/admin.py                       | 12 +++++++++--
 .../0025_user_deactivation_datetime.py        | 20 +++++++++++++++++++
 src/authentic2/custom_user/models.py          | 13 ++++++++++--
 tests/test_all.py                             |  1 +
 tests/test_api.py                             |  6 ++++--
 5 files changed, 46 insertions(+), 6 deletions(-)
 create mode 100644 src/authentic2/custom_user/migrations/0025_user_deactivation_datetime.py

diff --git a/src/authentic2/admin.py b/src/authentic2/admin.py
index d6bed0ea..ba2c3690 100644
--- a/src/authentic2/admin.py
+++ b/src/authentic2/admin.py
@@ -277,7 +277,7 @@ class AuthenticUserAdmin(UserAdmin):
         (_('Personal info'), {'fields': ('username', 'first_name', 'last_name', 'email')}),
         (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
                                        'groups')}),
-        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
+        (_('Important dates'), {'fields': ('last_login', 'date_joined', 'deactivation_datetime')}),
     )
     add_fieldsets = (
         (None, {
@@ -287,7 +287,8 @@ class AuthenticUserAdmin(UserAdmin):
     readonly_fields = ('uuid',)
     list_filter = UserAdmin.list_filter + (UserRealmListFilter, ExternalUserListFilter) + ('deleted',)
     list_display = ['__str__', 'ou', 'first_name', 'last_name', 'email', 'deleted']
-    actions = UserAdmin.actions + ['mark_as_deleted', 'unmark_as_deleted', 'apply_deletion']
+    actions = UserAdmin.actions + ['mark_as_inactive', 'mark_as_deleted',
+                                   'unmark_as_deleted', 'apply_deletion']
 
     def get_fieldsets(self, request, obj=None):
         fieldsets = deepcopy(super(AuthenticUserAdmin, self).get_fieldsets(request, obj))
@@ -327,6 +328,13 @@ class AuthenticUserAdmin(UserAdmin):
         kwargs['fields'] = fields
         return super(AuthenticUserAdmin, self).get_form(request, obj=obj, **kwargs)
 
+    @transaction.atomic
+    def mark_as_inactive(self, request, queryset):
+        timestamp = timezone.now()
+        for user in queryset:
+            user.mark_as_inactive(timestamp=timestamp)
+    mark_as_inactive.short_description = _('Mark as inactive')
+
     @transaction.atomic
     def mark_as_deleted(self, request, queryset):
         timestamp = timezone.now()
diff --git a/src/authentic2/custom_user/migrations/0025_user_deactivation_datetime.py b/src/authentic2/custom_user/migrations/0025_user_deactivation_datetime.py
new file mode 100644
index 00000000..ef681ed9
--- /dev/null
+++ b/src/authentic2/custom_user/migrations/0025_user_deactivation_datetime.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2021-02-09 09:37
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('custom_user', '0024_index_email_by_trigrams'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='deactivation_datetime',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='Deactivation datetime'),
+        ),
+    ]
diff --git a/src/authentic2/custom_user/models.py b/src/authentic2/custom_user/models.py
index a02fa85e..79e086a4 100644
--- a/src/authentic2/custom_user/models.py
+++ b/src/authentic2/custom_user/models.py
@@ -193,6 +193,10 @@ class User(AbstractBaseUser, PermissionMixin):
         verbose_name=_('Deletion date'),
         null=True,
         blank=True)
+    deactivation_datetime = models.DateTimeField(
+        verbose_name=_('Deactivation datetime'),
+        null=True,
+        blank=True)
 
     objects = UserManager.from_queryset(UserQuerySet)()
     attributes = AttributesDescriptor()
@@ -363,12 +367,17 @@ class User(AbstractBaseUser, PermissionMixin):
             del self._a2_attributes_cache
         return super(User, self).refresh_from_db(*args, **kwargs)
 
-    def mark_as_deleted(self, timestamp=None, force=True, save=True):
+    def mark_as_inactive(self, timestamp=None):
         self.is_active = False
+        self.deactivation_datetime = timestamp or timezone.now()
+        self.save(update_fields=['is_active', 'deactivation_datetime'])
+
+    def mark_as_deleted(self, timestamp=None, force=True, save=True):
+        self.mark_as_inactive(timestamp)
         if force or not self.deleted:
             self.deleted = timestamp or timezone.now()
         if save:
-            self.save(update_fields=['is_active', 'deleted'])
+            self.save(update_fields=['deleted'])
 
     def set_random_password(self):
         self.set_password(base64.b64encode(os.urandom(32)).decode('ascii'))
diff --git a/tests/test_all.py b/tests/test_all.py
index 4695990e..09bb9c26 100644
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -92,6 +92,7 @@ class SerializerTests(TestCase):
                     'password': '',
                     'ou': None,
                     'deleted': None,
+                    'deactivation_datetime': None,
                 }
             },
             {
diff --git a/tests/test_api.py b/tests/test_api.py
index 854ba29b..103b4eea 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -416,7 +416,8 @@ def test_api_users_create(settings, app, api_user):
                     'last_name_verified', 'date_joined', 'last_login',
                     'username', 'password', 'email', 'is_active', 'title',
                     'title_verified', 'modified', 'email_verified',
-                    'last_account_deletion_alert', 'deleted']) == set(resp.json.keys())
+                    'last_account_deletion_alert', 'deleted',
+                    'deactivation_datetime']) == set(resp.json.keys())
         assert resp.json['first_name'] == payload['first_name']
         assert resp.json['last_name'] == payload['last_name']
         assert resp.json['email'] == payload['email']
@@ -483,7 +484,8 @@ def test_api_users_create(settings, app, api_user):
                     'last_name_verified', 'date_joined', 'last_login',
                     'username', 'password', 'email', 'is_active', 'title',
                     'title_verified', 'modified', 'email_verified',
-                    'last_account_deletion_alert', 'deleted']) == set(resp.json.keys())
+                    'last_account_deletion_alert', 'deleted',
+                    'deactivation_datetime']) == set(resp.json.keys())
         user = get_user_model().objects.get(pk=resp.json['id'])
         assert AttributeValue.objects.with_owner(user).filter(verified=True).count() == 3
         assert AttributeValue.objects.with_owner(user).filter(verified=False).count() == 0
-- 
2.30.0

