Config model added to upload csv
PROD-1911
This commit is contained in:
@@ -42,7 +42,8 @@ from student.models import (
|
||||
UserProfile,
|
||||
UserTestGroup,
|
||||
BulkUnenrollConfiguration,
|
||||
AccountRecoveryConfiguration
|
||||
AccountRecoveryConfiguration,
|
||||
BulkChangeEnrollmentConfiguration
|
||||
)
|
||||
from student.roles import REGISTERED_ACCESS_ROLES
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -541,6 +542,7 @@ admin.site.register(AccountRecoveryConfiguration, ConfigurationModelAdmin)
|
||||
admin.site.register(DashboardConfiguration, ConfigurationModelAdmin)
|
||||
admin.site.register(RegistrationCookieConfiguration, ConfigurationModelAdmin)
|
||||
admin.site.register(BulkUnenrollConfiguration, ConfigurationModelAdmin)
|
||||
admin.site.register(BulkChangeEnrollmentConfiguration, ConfigurationModelAdmin)
|
||||
|
||||
|
||||
# We must first un-register the User model since it may also be registered by the auth app.
|
||||
|
||||
@@ -4,10 +4,11 @@ csv file.
|
||||
"""
|
||||
|
||||
|
||||
import csv
|
||||
import logging
|
||||
from os import path
|
||||
import unicodecsv
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -15,6 +16,8 @@ from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAttribute, User
|
||||
|
||||
from student.models import BulkChangeEnrollmentConfiguration
|
||||
|
||||
logger = logging.getLogger('student.management.commands.bulk_change_enrollment_csv')
|
||||
|
||||
|
||||
@@ -42,80 +45,126 @@ class Command(BaseCommand):
|
||||
""" Add argument to the command parser. """
|
||||
parser.add_argument(
|
||||
'--csv_file_path',
|
||||
required=True,
|
||||
required=False,
|
||||
help='Csv file path'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--file_from_database',
|
||||
action='store_true',
|
||||
help='Use file from the BulkChangeEnrollmentConfiguration model instead of the command line.',
|
||||
)
|
||||
|
||||
def get_file_from_database(self):
|
||||
""" Returns an options dictionary from the current SSPVerificationRetryConfig model. """
|
||||
|
||||
enrollment_config = BulkChangeEnrollmentConfiguration.current()
|
||||
if not enrollment_config.enabled:
|
||||
raise CommandError('BulkChangeEnrollmentConfiguration is disabled or empty, '
|
||||
'but --file_from_database from was requested.')
|
||||
|
||||
return enrollment_config.csv_file
|
||||
|
||||
def handle(self, *args, **options):
|
||||
""" Main handler for the command."""
|
||||
file_path = options['csv_file_path']
|
||||
file_path = options.get('csv_file_path', None)
|
||||
file_from_database = options['file_from_database']
|
||||
|
||||
if not path.isfile(file_path):
|
||||
raise CommandError("File not found.")
|
||||
if file_from_database:
|
||||
csv_file = self.get_file_from_database()
|
||||
self.change_enrollments(csv_file)
|
||||
|
||||
with open(file_path) as csv_file:
|
||||
course_key = None
|
||||
user = None
|
||||
file_reader = csv.DictReader(csv_file)
|
||||
headers = file_reader.fieldnames
|
||||
elif file_path:
|
||||
if not path.isfile(file_path):
|
||||
raise CommandError("File not found.")
|
||||
|
||||
if not ('course_id' in headers and 'mode' in headers and 'user' in headers):
|
||||
raise CommandError('Invalid input CSV file.')
|
||||
with open(file_path, 'rb') as csv_file:
|
||||
self.change_enrollments(csv_file)
|
||||
|
||||
for row in file_reader:
|
||||
else:
|
||||
CommandError('No file is provided. File is required')
|
||||
|
||||
def change_enrollments(self, csv_file):
|
||||
""" change the enrollments of the learners. """
|
||||
course_key = None
|
||||
user = None
|
||||
file_reader = unicodecsv.DictReader(csv_file)
|
||||
headers = file_reader.fieldnames
|
||||
|
||||
if not ('course_id' in headers and 'mode' in headers and 'user' in headers):
|
||||
raise CommandError('Invalid input CSV file.')
|
||||
|
||||
for row in list(file_reader):
|
||||
try:
|
||||
course_key = CourseKey.from_string(row['course_id'])
|
||||
except InvalidKeyError:
|
||||
logger.warning('Invalid or non-existent course id [{}]'.format(row['course_id']))
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=row['user'])
|
||||
except ObjectDoesNotExist:
|
||||
logger.warning('Invalid or non-existent user [{}]'.format(row['user']))
|
||||
|
||||
if course_key and user:
|
||||
try:
|
||||
course_key = CourseKey.from_string(row['course_id'])
|
||||
except InvalidKeyError:
|
||||
logger.warning('Invalid or non-existent course id [{}]'.format(row['course_id']))
|
||||
course_enrollment = self.get_course_enrollment(course_key, user)
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=row['user'])
|
||||
except:
|
||||
logger.warning('Invalid or non-existent user [{}]'.format(row['user']))
|
||||
if course_key and user:
|
||||
try:
|
||||
course_enrollment = CourseEnrollment.get_enrollment(user, course_key)
|
||||
# If student is not enrolled in course enroll the student in free mode
|
||||
if not course_enrollment:
|
||||
# try to create a enroll user in default course enrollment mode in case of
|
||||
# professional it will break because of no default course mode.
|
||||
try:
|
||||
course_enrollment = CourseEnrollment.get_or_create_enrollment(user=user,
|
||||
course_key=course_key)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# In case if no free mode is available.
|
||||
course_enrollment = None
|
||||
if course_enrollment:
|
||||
mode = row['mode']
|
||||
self.update_enrollment_mode(course_key, user, mode, course_enrollment)
|
||||
|
||||
if course_enrollment:
|
||||
# if student already had a enrollment and its mode is same as the provided one
|
||||
if course_enrollment.mode == row['mode']:
|
||||
logger.info("Student [%s] is already enrolled in Course [%s] in mode [%s].", user.username,
|
||||
course_key, course_enrollment.mode)
|
||||
# set the enrollment to active if its not already active.
|
||||
if not course_enrollment.is_active:
|
||||
course_enrollment.update_enrollment(is_active=True)
|
||||
else:
|
||||
# if student enrollment exists update it to new mode.
|
||||
with transaction.atomic():
|
||||
course_enrollment.update_enrollment(
|
||||
mode=row['mode'],
|
||||
is_active=True,
|
||||
skip_refund=True
|
||||
)
|
||||
course_enrollment.save()
|
||||
else:
|
||||
# if student enrollment do not exists directly enroll in new mode.
|
||||
CourseEnrollment.enroll(user=user, course_key=course_key, mode=row['mode'])
|
||||
|
||||
if row['mode'] == 'credit':
|
||||
enrollment_attrs = [{
|
||||
'namespace': 'credit',
|
||||
'name': 'provider_id',
|
||||
'value': course_key.org,
|
||||
}]
|
||||
CourseEnrollmentAttribute.add_enrollment_attr(enrollment=course_enrollment,
|
||||
data_list=enrollment_attrs)
|
||||
else:
|
||||
# if student enrollment do not exists directly enroll in new mode.
|
||||
CourseEnrollment.enroll(user=user, course_key=course_key, mode=row['mode'])
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.info("Unable to update student [%s] course [%s] enrollment to mode [%s] "
|
||||
"because of Exception [%s]", row['user'], row['course_id'], row['mode'], repr(e))
|
||||
|
||||
except Exception as e:
|
||||
logger.info("Unable to update student [%s] course [%s] enrollment to mode [%s] "
|
||||
"because of Exception [%s]", row['user'], row['course_id'], row['mode'], repr(e))
|
||||
def get_course_enrollment(self, course_key, user):
|
||||
"""
|
||||
If student is not enrolled in course enroll the student in free mode
|
||||
"""
|
||||
|
||||
course_enrollment = CourseEnrollment.get_enrollment(user, course_key)
|
||||
# If student is not enrolled in course enroll the student in free mode
|
||||
if not course_enrollment:
|
||||
# try to create a enroll user in default course enrollment mode in case of
|
||||
# professional it will break because of no default course mode.
|
||||
try:
|
||||
course_enrollment = CourseEnrollment.get_or_create_enrollment(user=user,
|
||||
course_key=course_key)
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# In case if no free mode is available.
|
||||
course_enrollment = None
|
||||
|
||||
return course_enrollment
|
||||
|
||||
def update_enrollment_mode(self, course_key, user, mode, course_enrollment):
|
||||
"""
|
||||
update the enrollment mode based on the learner existing state.
|
||||
"""
|
||||
# if student already had a enrollment and its mode is same as the provided one
|
||||
if course_enrollment.mode == mode:
|
||||
logger.info("Student [%s] is already enrolled in Course [%s] in mode [%s].", user.username,
|
||||
course_key, course_enrollment.mode)
|
||||
# set the enrollment to active if its not already active.
|
||||
if not course_enrollment.is_active:
|
||||
course_enrollment.update_enrollment(is_active=True)
|
||||
else:
|
||||
# if student enrollment exists update it to new mode.
|
||||
with transaction.atomic():
|
||||
course_enrollment.update_enrollment(
|
||||
mode=mode,
|
||||
is_active=True,
|
||||
skip_refund=True
|
||||
)
|
||||
course_enrollment.save()
|
||||
|
||||
if mode == 'credit':
|
||||
enrollment_attrs = [{'namespace': 'credit',
|
||||
'name': 'provider_id',
|
||||
'value': course_key.org
|
||||
}]
|
||||
CourseEnrollmentAttribute.add_enrollment_attr(enrollment=course_enrollment,
|
||||
data_list=enrollment_attrs)
|
||||
|
||||
@@ -5,7 +5,9 @@ from tempfile import NamedTemporaryFile
|
||||
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from testfixtures import LogCapture
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
@@ -15,6 +17,8 @@ from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from student.models import BulkChangeEnrollmentConfiguration
|
||||
|
||||
LOGGER_NAME = 'student.management.commands.bulk_change_enrollment_csv'
|
||||
|
||||
|
||||
@@ -120,3 +124,26 @@ class BulkChangeEnrollmentCSVTests(SharedModuleStoreTestCase):
|
||||
new_enrollment = CourseEnrollment.get_enrollment(user=enrollment.user, course_key=enrollment.course)
|
||||
self.assertEqual(new_enrollment.is_active, True)
|
||||
self.assertEqual(new_enrollment.mode, CourseMode.VERIFIED)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
def test_bulk_enrollment_from_config_model(self):
|
||||
""" Test all users are enrolled using the config model."""
|
||||
lines = "course_id,user,mode\n"
|
||||
for enrollment in self.enrollments:
|
||||
lines += str(enrollment.course.id) + "," + str(enrollment.user.username) + ",verified\n"
|
||||
|
||||
csv_file = SimpleUploadedFile(name='test.csv', content=lines.encode('utf-8'), content_type='text/csv')
|
||||
BulkChangeEnrollmentConfiguration.objects.create(enabled=True, csv_file=csv_file)
|
||||
call_command("bulk_change_enrollment_csv", "--file_from_database")
|
||||
|
||||
for enrollment in self.enrollments:
|
||||
new_enrollment = CourseEnrollment.get_enrollment(user=enrollment.user, course_key=enrollment.course)
|
||||
self.assertEqual(new_enrollment.is_active, True)
|
||||
self.assertEqual(new_enrollment.mode, CourseMode.VERIFIED)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
def test_command_error_for_config_model(self):
|
||||
""" Test command error raised if file_from_database is required and the config model is not enabled"""
|
||||
|
||||
with self.assertRaises(CommandError):
|
||||
call_command("bulk_change_enrollment_csv", "--file_from_database")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 2.2.14 on 2020-07-16 10:33
|
||||
|
||||
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', '0034_courseenrollmentcelebration'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BulkChangeEnrollmentConfiguration',
|
||||
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: course_id, username, mode', 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={
|
||||
'abstract': False,
|
||||
'ordering': ('-change_date',),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -2909,6 +2909,18 @@ class BulkUnenrollConfiguration(ConfigurationModel):
|
||||
)
|
||||
|
||||
|
||||
class BulkChangeEnrollmentConfiguration(ConfigurationModel):
|
||||
"""
|
||||
config model for the bulk_change_enrollment_csv 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: \
|
||||
course_id, username, mode")
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class UserAttribute(TimeStampedModel):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user