From 17d85591056137ef5ed1e584a88c28ed3851e1ba Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 27 Jan 2022 09:07:44 +0100 Subject: [PATCH] models: add User.email_verified_date field (#19634) --- src/authentic2/api_views.py | 4 +-- src/authentic2/csv_import.py | 2 +- .../0028_user_email_verified_date.py | 28 +++++++++++++++++++ src/authentic2/custom_user/models.py | 14 ++++++++++ src/authentic2/forms/registration.py | 2 +- src/authentic2/manager/user_views.py | 2 +- src/authentic2/views.py | 4 +-- src/authentic2_auth_oidc/backends.py | 4 +-- tests/test_all.py | 3 +- tests/test_api.py | 1 + tests/test_migrations.py | 12 ++++++++ tests/test_user_manager.py | 4 +-- 12 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 src/authentic2/custom_user/migrations/0028_user_email_verified_date.py diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py index 9220c8ab..fa27fc56 100644 --- a/src/authentic2/api_views.py +++ b/src/authentic2/api_views.py @@ -463,7 +463,7 @@ class BaseUserSerializer(serializers.ModelSerializer): if 'ou' in validated_data: self.check_perm('custom_user.change_user', validated_data.get('ou')) if validated_data.get('email') != instance.email and not validated_data.get('email_verified'): - instance.email_verified = False + instance.set_email_verified(False) super().update(instance, validated_data) for key, value in attributes.items(): if is_verified.get(key): @@ -863,7 +863,7 @@ class UsersAPI(api_mixins.GetOrCreateMixinView, HookMixin, ExceptionHandlerMixin if not serializer.is_valid(): response = {'result': 0, 'errors': serializer.errors} return Response(response, status.HTTP_400_BAD_REQUEST) - user.email_verified = False + user.set_email_verified(False) user.save() utils_misc.send_email_change_email(user, serializer.validated_data['email'], request=request) return Response({'result': 1}) diff --git a/src/authentic2/csv_import.py b/src/authentic2/csv_import.py index 462ac655..01670fb8 100644 --- a/src/authentic2/csv_import.py +++ b/src/authentic2/csv_import.py @@ -710,7 +710,7 @@ class UserCsvImporter: if getattr(user, cell.header.name) != cell.value: setattr(user, cell.header.name, cell.value) if cell.header.name == 'email' and cell.header.verified: - user.email_verified = True + user.set_email_verified(True) cell.action = 'updated' continue cell.action = 'nothing' diff --git a/src/authentic2/custom_user/migrations/0028_user_email_verified_date.py b/src/authentic2/custom_user/migrations/0028_user_email_verified_date.py new file mode 100644 index 00000000..8ad839db --- /dev/null +++ b/src/authentic2/custom_user/migrations/0028_user_email_verified_date.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.23 on 2022-01-27 08:03 + + +from django.db import migrations, models + + +def set_email_verified_date(apps, schema_editor): + User = apps.get_model('custom_user', 'User') + qs = User.objects.filter(email_verified=True, email_verified_date__isnull=True) + # set all unknown dates to date joined + qs.update(email_verified_date=models.F('date_joined')) + + +class Migration(migrations.Migration): + dependencies = [ + ('custom_user', '0027_user_deactivation_reason'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='email_verified_date', + field=models.DateTimeField( + default=None, blank=True, null=True, verbose_name='email verified date' + ), + ), + migrations.RunPython(set_email_verified_date, migrations.RunPython.noop), + ] diff --git a/src/authentic2/custom_user/models.py b/src/authentic2/custom_user/models.py index 366e42af..65142962 100644 --- a/src/authentic2/custom_user/models.py +++ b/src/authentic2/custom_user/models.py @@ -150,6 +150,9 @@ class User(AbstractBaseUser, PermissionMixin): last_name = models.CharField(_('last name'), max_length=128, blank=True) email = models.EmailField(_('email address'), blank=True, max_length=254, validators=[email_validator]) email_verified = models.BooleanField(default=False, verbose_name=_('email verified')) + email_verified_date = models.DateTimeField( + default=None, blank=True, null=True, verbose_name=_('email verified date') + ) is_staff = models.BooleanField( _('staff status'), default=False, @@ -428,6 +431,17 @@ class User(AbstractBaseUser, PermissionMixin): def get_absolute_url(self): return reverse('a2-manager-user-detail', kwargs={'pk': self.pk}) + def set_email_verified(self, value): + if isinstance(value, datetime.datetime): + self.email_verified = True + self.email_verified_date = value + elif bool(value): + self.email_verified = True + self.email_verified_date = timezone.now() + else: + self.email_verified = False + self.email_verified_date = None + class DeletedUser(models.Model): deleted = models.DateTimeField(verbose_name=_('Deletion date'), auto_now_add=True) diff --git a/src/authentic2/forms/registration.py b/src/authentic2/forms/registration.py index b4738186..a51ad1be 100644 --- a/src/authentic2/forms/registration.py +++ b/src/authentic2/forms/registration.py @@ -138,7 +138,7 @@ class RegistrationCompletionFormNoPassword(profile_forms.BaseUserForm): return BaseUserManager.normalize_email(email) def save(self, commit=True): - self.instance.email_verified = True + self.instance.set_email_verified(True) self.instance.is_active = True user = super().save(commit=commit) if commit and app_settings.A2_REGISTRATION_GROUPS: diff --git a/src/authentic2/manager/user_views.py b/src/authentic2/manager/user_views.py index 96155556..e64b443b 100644 --- a/src/authentic2/manager/user_views.py +++ b/src/authentic2/manager/user_views.py @@ -485,7 +485,7 @@ class UserEditView(OtherActionsMixin, ActionMixin, BaseEditView): def form_valid(self, form): if 'email' in form.changed_data: - self.object.email_verified = False + self.object.set_email_verified(False) self.object.save() response = super().form_valid(form) if form.has_changed(): diff --git a/src/authentic2/views.py b/src/authentic2/views.py index 1eab8dad..2a56ed72 100644 --- a/src/authentic2/views.py +++ b/src/authentic2/views.py @@ -246,7 +246,7 @@ class EmailChangeVerifyView(TemplateView): raise ValidationError(_('This email is already used by another account.')) old_email = user.email user.email = email - user.email_verified = True + user.set_email_verified(True) user.save() messages.info( request, _('your request for changing your email for {0} is successful').format(email) @@ -873,7 +873,7 @@ class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView): def form_valid(self, form): # Changing password by mail validate the email - form.user.email_verified = True + form.user.set_email_verified(True) form.save() hooks.call_hooks('event', name='password-reset-confirm', user=form.user, token=self.token, form=form) logger.info('password reset for user %s with token %r', self.user, self.token.uuid) diff --git a/src/authentic2_auth_oidc/backends.py b/src/authentic2_auth_oidc/backends.py index 6c75b321..b56f5658 100644 --- a/src/authentic2_auth_oidc/backends.py +++ b/src/authentic2_auth_oidc/backends.py @@ -310,8 +310,8 @@ class OIDCBackend(ModelBackend): if getattr(user, attribute) != value: logger.info('auth_oidc: set user %s attribute %s to value %s', user, attribute, value) setattr(user, attribute, value) - if attribute == 'email' and verified: - user.email_verified = True + if attribute == 'email': + user.set_email_verified(verified) save_user = True if user.ou != user_ou: diff --git a/tests/test_all.py b/tests/test_all.py index f31dfdbc..f71b19f1 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -70,6 +70,7 @@ class SerializerTests(TestCase): 'fields': { 'uuid': u.uuid, 'email_verified': False, + 'email_verified_date': None, 'username': 'john.doe', 'email': '', 'first_name': '', @@ -123,7 +124,7 @@ class SerializerTests(TestCase): expected = json.loads(json.dumps(expected, cls=DjangoJSONEncoder)) for obj in serializers.deserialize('json', result): obj.save() - self.assertEqual(json.loads(result), expected) + assert json.loads(result) == expected self.assertEqual(User.objects.count(), ucount + 1) self.assertEqual(Attribute.objects.count(), acount + 1) # first_name and last_name attribute value not recreated since they were not dumped diff --git a/tests/test_api.py b/tests/test_api.py index 4d487ab7..71a3bfe1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -64,6 +64,7 @@ USER_ATTRIBUTES_SET = { 'is_active', 'modified', 'email_verified', + 'email_verified_date', 'last_account_deletion_alert', 'deactivation', 'deactivation_reason', diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 6855129c..ed99329d 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -46,3 +46,15 @@ def test_migration_custom_user_0026_remove_user_deleted(transactional_db, migrat DeletedUser = new_apps.get_model('custom_user', 'DeletedUser') assert User.objects.count() == 1 assert DeletedUser.objects.count() == 1 + + +def test_migration_custom_user_0028_user_email_verified_date(transactional_db, migration): + old_apps = migration.before([('custom_user', '0027_user_deactivation_reason')]) + + User = old_apps.get_model('custom_user', 'User') + User.objects.create(email='john.doe@example.com', email_verified=True) + + new_apps = migration.apply([('custom_user', '0028_user_email_verified_date')]) + User = new_apps.get_model('custom_user', 'User') + user = User.objects.get() + assert user.email_verified_date == user.date_joined diff --git a/tests/test_user_manager.py b/tests/test_user_manager.py index f416a178..307284ab 100644 --- a/tests/test_user_manager.py +++ b/tests/test_user_manager.py @@ -918,7 +918,7 @@ def test_ou_hide_username(admin, app, db): def test_manager_edit_user_email_verified(app, simple_user, superuser_or_admin): - simple_user.email_verified = True + simple_user.set_email_verified(True) simple_user.save() url = '/manage/users/%s/edit/' % simple_user.pk @@ -959,7 +959,7 @@ def test_manager_email_verified_column_user(app, simple_user, superuser_or_admin resp = app.get('/manage/users/') assert not resp.html.find('span', {'class': 'verified'}) - simple_user.email_verified = True + simple_user.set_email_verified(True) simple_user.save() resp = app.get('/manage/users/') assert resp.html.find('span', {'class': 'verified'}).text == simple_user.email -- 2.34.1