diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index cb740c894e6d961bf40e6e009e7fb7fff8ab5a58..b9a7fd3731b2a29d54dbc731b6a70c66f399f1db 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -40,7 +40,8 @@ from student.models import ( UserAttribute, UserProfile, UserTestGroup, - BulkUnenrollConfiguration + BulkUnenrollConfiguration, + AccountRecoveryConfiguration ) from student.roles import REGISTERED_ACCESS_ROLES from xmodule.modulestore.django import modulestore @@ -492,6 +493,7 @@ class AllowedAuthUserAdmin(admin.ModelAdmin): admin.site.register(UserTestGroup) admin.site.register(Registration) admin.site.register(PendingNameChange) +admin.site.register(AccountRecoveryConfiguration, ConfigurationModelAdmin) admin.site.register(DashboardConfiguration, ConfigurationModelAdmin) admin.site.register(RegistrationCookieConfiguration, ConfigurationModelAdmin) admin.site.register(BulkUnenrollConfiguration, ConfigurationModelAdmin) diff --git a/common/djangoapps/student/management/commands/recover_account.py b/common/djangoapps/student/management/commands/recover_account.py index a7eba2075a1904921921828ccf959319c5b69943..6b70782285f385446d71322fdf9222af1280fa59 100644 --- a/common/djangoapps/student/management/commands/recover_account.py +++ b/common/djangoapps/student/management/commands/recover_account.py @@ -17,6 +17,7 @@ from django.utils.http import int_to_base36 from edx_ace import ace from edx_ace.recipient import Recipient +from student.models import AccountRecoveryConfiguration from openedx.core.djangoapps.ace_common.template_context import get_base_template_context from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers @@ -49,18 +50,24 @@ class Command(BaseCommand): """ Add argument to the command parser. """ parser.add_argument( '--csv_file_path', - required=True, + required=False, help='Csv file path' ) def handle(self, *args, **options): """ Main handler for the command.""" file_path = options['csv_file_path'] - - if not path.isfile(file_path): - raise CommandError('File not found.') - - with open(file_path, 'rb') as csv_file: + if file_path: + if not path.isfile(file_path): + raise CommandError('File not found.') + + with open(file_path, 'rb') as csv_file: + csv_reader = list(unicodecsv.DictReader(csv_file)) + else: + csv_file = AccountRecoveryConfiguration.current().csv_file + if not csv_file: + logger.error('No csv file found. Please make sure csv file is uploaded') + return csv_reader = list(unicodecsv.DictReader(csv_file)) successful_updates = [] diff --git a/common/djangoapps/student/management/tests/test_recover_account.py b/common/djangoapps/student/management/tests/test_recover_account.py index 8e3d09d4cfdb3b34b32f30ad45d8a4ea8e781455..7e10b911aad835a38317f50aae6757e6378b3581 100644 --- a/common/djangoapps/student/management/tests/test_recover_account.py +++ b/common/djangoapps/student/management/tests/test_recover_account.py @@ -6,12 +6,15 @@ from tempfile import NamedTemporaryFile import six from django.core import mail +from django.contrib.auth import get_user_model from django.core.management import call_command, CommandError +from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase, RequestFactory from testfixtures import LogCapture from student.tests.factories import UserFactory +from student.models import AccountRecoveryConfiguration LOGGER_NAME = 'student.management.commands.recover_account' @@ -101,4 +104,17 @@ class RecoverAccountTests(TestCase): log.check_present( (LOGGER_NAME, 'INFO', expected_message) + ) + + def test_account_recovery_from_config_model(self): + """Verify learners account recovery using config model.""" + lines = 'username,email,new_email\namy,amy@edx.com,amy@newemail.com\n' + + csv_file = SimpleUploadedFile(name='test.csv', content=lines.encode('utf-8'), content_type='text/csv') + AccountRecoveryConfiguration.objects.create(enabled=True, csv_file=csv_file) + + call_command("recover_account") + + email = get_user_model().objects.get(pk=self.user.pk).email + self.assertEqual(email, 'amy@newemail.com') diff --git a/common/djangoapps/student/migrations/0031_auto_20200317_1122.py b/common/djangoapps/student/migrations/0031_auto_20200317_1122.py new file mode 100644 index 0000000000000000000000000000000000000000..26e37f5bedec7c30a5623b0110a511f7a569b9c3 --- /dev/null +++ b/common/djangoapps/student/migrations/0031_auto_20200317_1122.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-03-17 11:22 +from __future__ import unicode_literals + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('student', '0030_userprofile_phone_number'), + ] + + operations = [ + migrations.CreateModel( + name='AccountRecoveryConfiguration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), + ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), + ('csv_file', models.FileField(help_text='It expect that the data will be provided in a csv file format with first row being the header and columns will be as follows: username, email, new_email', upload_to='', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['csv'])])), + ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')), + ], + options={ + 'ordering': ('-change_date',), + 'abstract': False, + }, + ) + ] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 48b6c80c4245650c4969954086f82ca62aa8559a..54c03864fa46cae7ac752fdb40e2fcf1c2bce857 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -2996,3 +2996,15 @@ class AllowedAuthUser(TimeStampedModel): "this model then that employee can login via third party authentication backend only."), unique=True, ) + + +class AccountRecoveryConfiguration(ConfigurationModel): + """ + configuration model for recover account management command + """ + csv_file = models.FileField( + validators=[FileExtensionValidator(allowed_extensions=[u'csv'])], + help_text=_(u"It expect that the data will be provided in a csv file format with \ + first row being the header and columns will be as follows: \ + username, email, new_email") + )