Config model added to upload csv

PROD-1911
This commit is contained in:
adeelehsan
2020-07-15 13:20:11 +05:00
parent d5f5891ae3
commit f2e989183c
5 changed files with 186 additions and 65 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -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")

View File

@@ -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',),
},
),
]

View File

@@ -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):
"""