From f3e20d1c94585837d919ba3c08fd01dc8a6cf8db Mon Sep 17 00:00:00 2001
From: Valentin Deniaud <vdeniaud@entrouvert.com>
Date: Tue, 15 Oct 2019 17:32:19 +0200
Subject: [PATCH 2/2] csv_import: allow setting user password and sending mail
 (#35774)

---
 src/authentic2/csv_import.py | 39 ++++++++++++++++++++++++++++++--
 tests/test_csv_import.py     | 44 +++++++++++++++++++++++++++++++++++-
 2 files changed, 80 insertions(+), 3 deletions(-)

diff --git a/src/authentic2/csv_import.py b/src/authentic2/csv_import.py
index 029e58af..aed57a17 100644
--- a/src/authentic2/csv_import.py
+++ b/src/authentic2/csv_import.py
@@ -36,7 +36,9 @@ from authentic2 import app_settings
 from authentic2.a2_rbac.utils import get_default_ou
 from authentic2.custom_user.models import User
 from authentic2.forms.profile import modelform_factory, BaseUserForm
-from authentic2.models import Attribute, AttributeValue, UserExternalId
+from authentic2.models import Attribute, AttributeValue, UserExternalId, PasswordReset
+from authentic2.utils import (send_password_reset_mail, generate_password,
+                              send_templated_mail)
 
 Role = get_role_model()
 
@@ -197,7 +199,10 @@ SOURCE_ID = '_source_id'
 SOURCE_COLUMNS = set([SOURCE_NAME, SOURCE_ID])
 ROLE_NAME = '_role_name'
 ROLE_SLUG = '_role_slug'
-SPECIAL_COLUMNS = SOURCE_COLUMNS | {ROLE_NAME, ROLE_SLUG}
+REGISTRATION = '@registration'
+REGISTRATION_RESET_EMAIL = 'send-set-password-link'
+REGISTRATION_PASSWORD_EMAIL = 'send-password'
+SPECIAL_COLUMNS = SOURCE_COLUMNS | {ROLE_NAME, ROLE_SLUG, REGISTRATION}
 
 
 class ImportUserForm(BaseUserForm):
@@ -207,6 +212,14 @@ class ImportUserForm(BaseUserForm):
     locals()[ROLE_SLUG] = forms.CharField(
         label=_('Role slug'),
         required=False)
+    choices = [
+        (REGISTRATION_RESET_EMAIL, _('Email user so they can set a password')),
+        (REGISTRATION_PASSWORD_EMAIL, _('Email user with a password they will have to change')),
+    ]
+    locals()[REGISTRATION] = forms.ChoiceField(
+        choices,
+        label=_('Registration option'),
+        required=False)
 
     def clean(self):
         super(BaseUserForm, self).clean()
@@ -593,6 +606,7 @@ class UserCsvImporter(object):
 
         if not user:
             user = User(ou=self.ou)
+            user.set_password(generate_password())
 
         for cell in row.cells:
             if not cell.header.field:
@@ -639,6 +653,8 @@ class UserCsvImporter(object):
                 continue
             if cell.header.name in {ROLE_NAME, ROLE_SLUG}:
                 success &= self.add_role(cell, user, do_clear=True)
+            elif cell.header.name == REGISTRATION and row.action == 'create':
+                success &= self.registration_option(cell, user)
 
         setattr(self, row.action + 'd', getattr(self, row.action + 'd') + 1)
         return success
@@ -664,3 +680,22 @@ class UserCsvImporter(object):
             user.roles.add(role)
         cell.action = 'updated'
         return True
+
+    def registration_option(self, cell, user):
+        if cell.value == REGISTRATION_RESET_EMAIL:
+            send_password_reset_mail(
+                user,
+                template_names=['authentic2/manager/user_create_registration_email',
+                                'authentic2/password_reset'],
+                next_url='/accounts/',
+                context={'user': user})
+        elif cell.value == REGISTRATION_PASSWORD_EMAIL:
+            new_password = generate_password()
+            user.set_password(new_password)
+            user.save()
+            PasswordReset.objects.get_or_create(user=user)
+            send_templated_mail(
+                user,
+                'authentic2/manager/new-account-notification',
+                context={'new_password': new_password, 'user': user})
+        return True
diff --git a/tests/test_csv_import.py b/tests/test_csv_import.py
index b07449c8..0b899d32 100644
--- a/tests/test_csv_import.py
+++ b/tests/test_csv_import.py
@@ -21,10 +21,12 @@ import pytest
 
 import io
 
+from django.core import mail
+
 from django_rbac.utils import get_role_model
 
 from authentic2.custom_user.models import User
-from authentic2.models import Attribute
+from authentic2.models import Attribute, PasswordReset
 from authentic2.a2_rbac.utils import get_default_ou
 
 from authentic2.csv_import import CsvImporter, UserCsvImporter, CsvHeader, Error, LineError
@@ -164,6 +166,7 @@ x,x,x,x'''
     assert thomas.last_name == 'Noël'
     assert thomas.attributes.last_name == 'Noël'
     assert thomas.attributes.phone == '1234'
+    assert thomas.password
 
     fpeters = User.objects.get(email='fpeters@entrouvert.com')
     assert fpeters.ou == get_default_ou()
@@ -476,3 +479,42 @@ tnoel@entrouvert.com,test_name'''
     assert not importer.run()
     assert importer.has_errors
     assert importer.errors[0].code == 'invalid-role-column'
+
+
+def test_csv_registration_options(profile, user_csv_importer_factory):
+    content = '''email key,first_name,last_name,@registration
+tnoel@entrouvert.com,Thomas,Noël,'''
+
+    importer = user_csv_importer_factory(content + 'send-password')
+    assert importer.run()
+    thomas = User.objects.get(email='tnoel@entrouvert.com')
+    assert len(mail.outbox) == 1
+    assert PasswordReset.objects.get(user=thomas)
+
+    del mail.outbox[0]
+    password = thomas.password
+    importer = user_csv_importer_factory(content + 'send-password')
+    assert importer.run()
+    thomas.refresh_from_db()
+    assert thomas.password == password
+    assert not mail.outbox
+
+    thomas.delete()
+    assert not mail.outbox
+    importer = user_csv_importer_factory(content + 'send-set-password-link')
+    assert importer.run()
+    thomas = User.objects.get(email='tnoel@entrouvert.com')
+    assert len(mail.outbox) == 1
+
+    password = thomas.password
+    del mail.outbox[0]
+    importer = user_csv_importer_factory(content + 'send-set-password-link')
+    assert importer.run()
+    thomas.refresh_from_db()
+    assert thomas.password == password
+    assert not mail.outbox
+
+    importer = user_csv_importer_factory(content + 'invalid-option')
+    assert importer.run()
+    assert importer.has_errors
+    assert importer.rows[0].cells[-1].errors[0].code == 'data-error'
-- 
2.20.1

