Commit 3685b1ad by Troy Sankey

More student management commands cleanup for Django 1.11

Seems like transfer_students.py was missed in the last attempt to
cleanup commands in this app.
parent 877c9faf
""" """
Transfer Student Management Command Transfer Student Management Command
""" """
from __future__ import print_function, unicode_literals
from textwrap import dedent
from six import text_type
from django.contrib.auth.models import User
from django.db import transaction from django.db import transaction
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from optparse import make_option
from django.contrib.auth.models import User
from student.models import CourseEnrollment
from shoppingcart.models import CertificateItem from shoppingcart.models import CertificateItem
from student.models import CourseEnrollment
from track.management.tracked_command import TrackedCommand from track.management.tracked_command import TrackedCommand
class TransferStudentError(Exception): class TransferStudentError(Exception):
"""Generic Error when handling student transfers.""" """
Generic Error when handling student transfers.
"""
pass pass
class Command(TrackedCommand): class Command(TrackedCommand):
"""Management Command for transferring students from one course to new courses."""
help = """
This command takes two course ids as input and transfers
all students enrolled in one course into the other. This will
remove them from the first class and enroll them in the specified
class(es) in the same mode as the first one. eg. honor, verified,
audit.
example:
# Transfer students from the old demoX class to a new one.
manage.py ... transfer_students -f edX/Open_DemoX/edx_demo_course -t edX/Open_DemoX/new_demoX
# Transfer students from old course to new, with original certificate items.
manage.py ... transfer_students -f edX/Open_DemoX/edx_demo_course -t edX/Open_DemoX/new_demoX -c true
# Transfer students from the old demoX class into two new classes.
manage.py ... transfer_students -f edX/Open_DemoX/edx_demo_course
-t edX/Open_DemoX/new_demoX,edX/Open_DemoX/edX_Insider
""" """
Transfer students enrolled in one course into one or more other courses.
option_list = TrackedCommand.option_list + ( This will remove them from the first course. Their enrollment mode (i.e.
make_option('-f', '--from', honor, verified, audit, etc.) will persist into the other course(s).
metavar='SOURCE_COURSE', """
dest='source_course', help = dedent(__doc__)
help='The course to transfer students from.'),
make_option('-t', '--to', def add_arguments(self, parser):
metavar='DEST_COURSE_LIST', parser.add_argument('-f', '--from',
dest='dest_course_list', metavar='SOURCE_COURSE',
help='The new course(es) to enroll the student into.'), dest='source_course',
make_option('-c', '--transfer-certificates', required=True,
metavar='TRANSFER_CERTIFICATES', help='the course to transfer students from')
dest='transfer_certificates', parser.add_argument('-t', '--to',
help="If True, try to transfer certificate items to the new course.") nargs='+',
) metavar='DEST_COURSE',
dest='dest_course_list',
required=True,
help='the new course(s) to enroll the student into')
parser.add_argument('-c', '--transfer-certificates',
action='store_true',
help='try to transfer certificate items to the new course')
@transaction.atomic @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
source_key = CourseKey.from_string(options.get('source_course', '')) source_key = CourseKey.from_string(options['source_course'])
dest_keys = [] dest_keys = []
for course_key in options.get('dest_course_list', '').split(','): for course_key in options['dest_course_list']:
dest_keys.append(CourseKey.from_string(course_key)) dest_keys.append(CourseKey.from_string(course_key))
if not source_key or not dest_keys: if options['transfer_certificates'] and len(dest_keys) > 1:
raise TransferStudentError(u"Must have a source course and destination course specified.") raise TransferStudentError('Cannot transfer certificate items from one course to many.')
tc_option = options.get('transfer_certificates', '')
transfer_certificates = ('true' == tc_option.lower()) if tc_option else False
if transfer_certificates and len(dest_keys) != 1:
raise TransferStudentError(u"Cannot transfer certificate items from one course to many.")
source_students = User.objects.filter( source_students = User.objects.filter(
courseenrollment__course_id=source_key courseenrollment__course_id=source_key
...@@ -73,7 +63,7 @@ class Command(TrackedCommand): ...@@ -73,7 +63,7 @@ class Command(TrackedCommand):
for user in source_students: for user in source_students:
with transaction.atomic(): with transaction.atomic():
print "Moving {}.".format(user.username) print('Moving {}.'.format(user.username))
# Find the old enrollment. # Find the old enrollment.
enrollment = CourseEnrollment.objects.get( enrollment = CourseEnrollment.objects.get(
user=user, user=user,
...@@ -84,14 +74,14 @@ class Command(TrackedCommand): ...@@ -84,14 +74,14 @@ class Command(TrackedCommand):
mode = enrollment.mode mode = enrollment.mode
old_is_active = enrollment.is_active old_is_active = enrollment.is_active
CourseEnrollment.unenroll(user, source_key, skip_refund=True) CourseEnrollment.unenroll(user, source_key, skip_refund=True)
print u"Unenrolled {} from {}".format(user.username, unicode(source_key)) print('Unenrolled {} from {}'.format(user.username, text_type(source_key)))
for dest_key in dest_keys: for dest_key in dest_keys:
if CourseEnrollment.is_enrolled(user, dest_key): if CourseEnrollment.is_enrolled(user, dest_key):
# Un Enroll from source course but don't mess # Un Enroll from source course but don't mess
# with the enrollment in the destination course. # with the enrollment in the destination course.
msg = u"Skipping {}, already enrolled in destination course {}" msg = 'Skipping {}, already enrolled in destination course {}'
print msg.format(user.username, unicode(dest_key)) print(msg.format(user.username, text_type(dest_key)))
else: else:
new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode) new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode)
...@@ -100,12 +90,13 @@ class Command(TrackedCommand): ...@@ -100,12 +90,13 @@ class Command(TrackedCommand):
if not old_is_active: if not old_is_active:
new_enrollment.update_enrollment(is_active=False, skip_refund=True) new_enrollment.update_enrollment(is_active=False, skip_refund=True)
if transfer_certificates: if options['transfer_certificates']:
self._transfer_certificate_item(source_key, enrollment, user, dest_keys, new_enrollment) self._transfer_certificate_item(source_key, enrollment, user, dest_keys, new_enrollment)
@staticmethod @staticmethod
def _transfer_certificate_item(source_key, enrollment, user, dest_keys, new_enrollment): def _transfer_certificate_item(source_key, enrollment, user, dest_keys, new_enrollment):
""" Transfer the certificate item from one course to another. """
Transfer the certificate item from one course to another.
Do not use this generally, since certificate items are directly associated with a particular purchase. Do not use this generally, since certificate items are directly associated with a particular purchase.
This should only be used when a single course to a new location. This cannot be used when transferring This should only be used when a single course to a new location. This cannot be used when transferring
...@@ -128,7 +119,7 @@ class Command(TrackedCommand): ...@@ -128,7 +119,7 @@ class Command(TrackedCommand):
course_enrollment=enrollment course_enrollment=enrollment
) )
except CertificateItem.DoesNotExist: except CertificateItem.DoesNotExist:
print u"No certificate for {}".format(user) print('No certificate for {}'.format(user))
return return
certificate_item.course_id = dest_keys[0] certificate_item.course_id = dest_keys[0]
......
""" """
Tests the transfer student management command Tests the transfer student management command
""" """
from django.conf import settings
from mock import patch, call
from opaque_keys.edx import locator
import unittest import unittest
import ddt
from shoppingcart.models import Order, CertificateItem # pylint: disable=import-error from mock import call, patch
from six import text_type
import ddt
from course_modes.models import CourseMode from course_modes.models import CourseMode
from student.management.commands import transfer_students from django.conf import settings
from student.models import CourseEnrollment, EVENT_NAME_ENROLLMENT_DEACTIVATED, \ from django.core.management import call_command
EVENT_NAME_ENROLLMENT_ACTIVATED, EVENT_NAME_ENROLLMENT_MODE_CHANGED from opaque_keys.edx import locator
from shoppingcart.models import CertificateItem, Order # pylint: disable=import-error
from student.models import (
EVENT_NAME_ENROLLMENT_ACTIVATED,
EVENT_NAME_ENROLLMENT_DEACTIVATED,
EVENT_NAME_ENROLLMENT_MODE_CHANGED,
CourseEnrollment
)
from student.signals import UNENROLL_DONE from student.signals import UNENROLL_DONE
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -21,13 +27,17 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -21,13 +27,17 @@ from xmodule.modulestore.tests.factories import CourseFactory
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt @ddt.ddt
class TestTransferStudents(ModuleStoreTestCase): class TestTransferStudents(ModuleStoreTestCase):
"""Tests for transferring students between courses.""" """
Tests for transferring students between courses.
"""
PASSWORD = 'test' PASSWORD = 'test'
signal_fired = False signal_fired = False
def setUp(self, **kwargs): def setUp(self, **kwargs):
"""Connect a stub receiver, and analytics event tracking.""" """
Connect a stub receiver, and analytics event tracking.
"""
super(TestTransferStudents, self).setUp() super(TestTransferStudents, self).setUp()
UNENROLL_DONE.connect(self.assert_unenroll_signal) UNENROLL_DONE.connect(self.assert_unenroll_signal)
...@@ -37,13 +47,17 @@ class TestTransferStudents(ModuleStoreTestCase): ...@@ -37,13 +47,17 @@ class TestTransferStudents(ModuleStoreTestCase):
self.addCleanup(UNENROLL_DONE.disconnect, self.assert_unenroll_signal) self.addCleanup(UNENROLL_DONE.disconnect, self.assert_unenroll_signal)
def assert_unenroll_signal(self, skip_refund=False, **kwargs): # pylint: disable=unused-argument def assert_unenroll_signal(self, skip_refund=False, **kwargs): # pylint: disable=unused-argument
""" Signal Receiver stub for testing that the unenroll signal was fired. """ """
Signal Receiver stub for testing that the unenroll signal was fired.
"""
self.assertFalse(self.signal_fired) self.assertFalse(self.signal_fired)
self.assertTrue(skip_refund) self.assertTrue(skip_refund)
self.signal_fired = True self.signal_fired = True
def test_transfer_students(self): def test_transfer_students(self):
""" Verify the transfer student command works as intended. """ """
Verify the transfer student command works as intended.
"""
student = UserFactory.create() student = UserFactory.create()
student.set_password(self.PASSWORD) student.set_password(self.PASSWORD)
student.save() student.save()
...@@ -52,7 +66,7 @@ class TestTransferStudents(ModuleStoreTestCase): ...@@ -52,7 +66,7 @@ class TestTransferStudents(ModuleStoreTestCase):
original_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0') original_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0')
course = self._create_course(original_course_location) course = self._create_course(original_course_location)
# Enroll the student in 'verified' # Enroll the student in 'verified'
CourseEnrollment.enroll(student, course.id, mode="verified") CourseEnrollment.enroll(student, course.id, mode='verified')
# Create and purchase a verified cert for the original course. # Create and purchase a verified cert for the original course.
self._create_and_purchase_verified(student, course.id) self._create_and_purchase_verified(student, course.id)
...@@ -64,13 +78,15 @@ class TestTransferStudents(ModuleStoreTestCase): ...@@ -64,13 +78,15 @@ class TestTransferStudents(ModuleStoreTestCase):
# New Course 2 # New Course 2
course_location_two = locator.CourseLocator('Org2', 'Course2', 'Run2') course_location_two = locator.CourseLocator('Org2', 'Course2', 'Run2')
new_course_two = self._create_course(course_location_two) new_course_two = self._create_course(course_location_two)
original_key = unicode(course.id) original_key = text_type(course.id)
new_key_one = unicode(new_course_one.id) new_key_one = text_type(new_course_one.id)
new_key_two = unicode(new_course_two.id) new_key_two = text_type(new_course_two.id)
# Run the actual management command # Run the actual management command
transfer_students.Command().handle( call_command(
source_course=original_key, dest_course_list=new_key_one + "," + new_key_two 'transfer_students',
'--from', original_key,
'--to', new_key_one, new_key_two,
) )
self.assertTrue(self.signal_fired) self.assertTrue(self.signal_fired)
...@@ -123,7 +139,9 @@ class TestTransferStudents(ModuleStoreTestCase): ...@@ -123,7 +139,9 @@ class TestTransferStudents(ModuleStoreTestCase):
self.assertEquals(target_certs[0].order.status, 'purchased') self.assertEquals(target_certs[0].order.status, 'purchased')
def _create_course(self, course_location): def _create_course(self, course_location):
""" Creates a course """ """
Creates a course
"""
return CourseFactory.create( return CourseFactory.create(
org=course_location.org, org=course_location.org,
number=course_location.course, number=course_location.course,
...@@ -131,10 +149,12 @@ class TestTransferStudents(ModuleStoreTestCase): ...@@ -131,10 +149,12 @@ class TestTransferStudents(ModuleStoreTestCase):
) )
def _create_and_purchase_verified(self, student, course_id): def _create_and_purchase_verified(self, student, course_id):
""" Creates a verified mode for the course and purchases it for the student. """ """
Creates a verified mode for the course and purchases it for the student.
"""
course_mode = CourseMode(course_id=course_id, course_mode = CourseMode(course_id=course_id,
mode_slug="verified", mode_slug='verified',
mode_display_name="verified cert", mode_display_name='verified cert',
min_price=50) min_price=50)
course_mode.save() course_mode.save()
# When there is no expiration date on a verified mode, the user can always get a refund # When there is no expiration date on a verified mode, the user can always get a refund
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment