Commit 08fb4950 by David Baumgold

Merge pull request #2302 from edx/db/django-command-migrate-to-split

Django command for migrating courses to split-mongo (and deleting)
parents 31593699 dd627eda
......@@ -49,6 +49,7 @@ coverage.xml
cover/
cover_html/
reports/
jscover.log
jscover.log.*
### Installation artifacts
......
"""
Django management command to migrate a course from the old Mongo modulestore
to the new split-Mongo modulestore.
"""
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.split_migrator import SplitMigrator
from xmodule.modulestore import InvalidLocationError
from xmodule.modulestore.django import loc_mapper
def user_from_str(identifier):
"""
Return a user identified by the given string. The string could be an email
address, or a stringified integer corresponding to the ID of the user in
the database. If no user could be found, a User.DoesNotExist exception
will be raised.
"""
try:
user_id = int(identifier)
except ValueError:
return User.objects.get(email=identifier)
else:
return User.objects.get(id=user_id)
class Command(BaseCommand):
"Migrate a course from old-Mongo to split-Mongo"
help = "Migrate a course from old-Mongo to split-Mongo"
args = "location email <locator>"
def parse_args(self, *args):
"""
Return a three-tuple of (location, user, locator_string).
If the user didn't specify a locator string, the third return value
will be None.
"""
if len(args) < 2:
raise CommandError(
"migrate_to_split requires at least two arguments: "
"a location and a user identifier (email or ID)"
)
try:
location = Location(args[0])
except InvalidLocationError:
raise CommandError("Invalid location string {}".format(args[0]))
try:
user = user_from_str(args[1])
except User.DoesNotExist:
raise CommandError("No user found identified by {}".format(args[1]))
try:
package_id = args[2]
except IndexError:
package_id = None
return location, user, package_id
def handle(self, *args, **options):
location, user, package_id = self.parse_args(*args)
migrator = SplitMigrator(
draft_modulestore=modulestore('default'),
direct_modulestore=modulestore('direct'),
split_modulestore=modulestore('split'),
loc_mapper=loc_mapper(),
)
migrator.migrate_mongo_course(location, user, package_id)
"""
Django management command to rollback a migration to split. The way to do this
is to delete the course from the split mongo datastore.
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError
from xmodule.modulestore.locator import CourseLocator
class Command(BaseCommand):
"Rollback a course that was migrated to the split Mongo datastore"
help = "Rollback a course that was migrated to the split Mongo datastore"
args = "locator"
def handle(self, *args, **options):
if len(args) < 1:
raise CommandError(
"rollback_split_course requires at least one argument (locator)"
)
try:
locator = CourseLocator(url=args[0])
except ValueError:
raise CommandError("Invalid locator string {}".format(args[0]))
location = loc_mapper().translate_locator_to_location(locator, get_course=True)
if not location:
raise CommandError(
"This course does not exist in the old Mongo store. "
"This command is designed to rollback a course, not delete "
"it entirely."
)
old_mongo_course = modulestore('direct').get_item(location)
if not old_mongo_course:
raise CommandError(
"This course does not exist in the old Mongo store. "
"This command is designed to rollback a course, not delete "
"it entirely."
)
try:
modulestore('split').delete_course(locator.package_id)
except ItemNotFoundError:
raise CommandError("No course found with locator {}".format(locator))
print(
'Course rolled back successfully. To delete this course entirely, '
'call the "delete_course" management command.'
)
"""
Unittests for migrating a course to split mongo
"""
import unittest
from django.contrib.auth.models import User
from django.core.management import CommandError, call_command
from django.test.utils import override_settings
from contentstore.management.commands.migrate_to_split import Command
from contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.locator import CourseLocator
# pylint: disable=E1101
class TestArgParsing(unittest.TestCase):
"""
Tests for parsing arguments for the `migrate_to_split` management command
"""
def setUp(self):
self.command = Command()
def test_no_args(self):
errstring = "migrate_to_split requires at least two arguments"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle()
def test_invalid_location(self):
errstring = "Invalid location string"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("foo", "bar")
def test_nonexistant_user_id(self):
errstring = "No user found identified by 99"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("i4x://org/course/category/name", "99")
def test_nonexistant_user_email(self):
errstring = "No user found identified by fake@example.com"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("i4x://org/course/category/name", "fake@example.com")
@override_settings(MODULESTORE=TEST_MODULESTORE)
class TestMigrateToSplit(ModuleStoreTestCase):
"""
Unit tests for migrating a course from old mongo to split mongo
"""
def setUp(self):
super(TestMigrateToSplit, self).setUp()
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
self.user = User.objects.create_user(uname, email, password)
self.course = CourseFactory()
def test_user_email(self):
call_command(
"migrate_to_split",
str(self.course.location),
str(self.user.email),
)
locator = loc_mapper().translate_location(self.course.id, self.course.location)
course_from_split = modulestore('split').get_course(locator)
self.assertIsNotNone(course_from_split)
def test_user_id(self):
call_command(
"migrate_to_split",
str(self.course.location),
str(self.user.id),
)
locator = loc_mapper().translate_location(self.course.id, self.course.location)
course_from_split = modulestore('split').get_course(locator)
self.assertIsNotNone(course_from_split)
def test_locator_string(self):
call_command(
"migrate_to_split",
str(self.course.location),
str(self.user.id),
"org.dept.name.run",
)
locator = CourseLocator(package_id="org.dept.name.run", branch="published")
course_from_split = modulestore('split').get_course(locator)
self.assertIsNotNone(course_from_split)
"""
Unittests for deleting a split mongo course
"""
import unittest
from StringIO import StringIO
from mock import patch
from django.contrib.auth.models import User
from django.core.management import CommandError, call_command
from django.test.utils import override_settings
from contentstore.management.commands.rollback_split_course import Command
from contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.persistent_factories import PersistentCourseFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.split_migrator import SplitMigrator
# pylint: disable=E1101
class TestArgParsing(unittest.TestCase):
"""
Tests for parsing arguments for the `rollback_split_course` management command
"""
def setUp(self):
self.command = Command()
def test_no_args(self):
errstring = "rollback_split_course requires at least one argument"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle()
def test_invalid_locator(self):
errstring = "Invalid locator string !?!"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("!?!")
@override_settings(MODULESTORE=TEST_MODULESTORE)
class TestRollbackSplitCourseNoOldMongo(ModuleStoreTestCase):
"""
Unit tests for rolling back a split-mongo course from command line,
where the course doesn't exist in the old mongo store
"""
def setUp(self):
super(TestRollbackSplitCourseNoOldMongo, self).setUp()
self.course = PersistentCourseFactory()
def test_no_old_course(self):
locator = self.course.location
errstring = "course does not exist in the old Mongo store"
with self.assertRaisesRegexp(CommandError, errstring):
Command().handle(str(locator))
@override_settings(MODULESTORE=TEST_MODULESTORE)
class TestRollbackSplitCourseNoSplitMongo(ModuleStoreTestCase):
"""
Unit tests for rolling back a split-mongo course from command line,
where the course doesn't exist in the split mongo store
"""
def setUp(self):
super(TestRollbackSplitCourseNoSplitMongo, self).setUp()
self.old_course = CourseFactory()
def test_nonexistent_locator(self):
locator = loc_mapper().translate_location(self.old_course.id, self.old_course.location)
errstring = "No course found with locator"
with self.assertRaisesRegexp(CommandError, errstring):
Command().handle(str(locator))
@override_settings(MODULESTORE=TEST_MODULESTORE)
class TestRollbackSplitCourse(ModuleStoreTestCase):
"""
Unit tests for rolling back a split-mongo course from command line
"""
def setUp(self):
super(TestRollbackSplitCourse, self).setUp()
self.old_course = CourseFactory()
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
self.user = User.objects.create_user(uname, email, password)
# migrate old course to split
migrator = SplitMigrator(
draft_modulestore=modulestore('default'),
direct_modulestore=modulestore('direct'),
split_modulestore=modulestore('split'),
loc_mapper=loc_mapper(),
)
migrator.migrate_mongo_course(self.old_course.location, self.user)
locator = loc_mapper().translate_location(self.old_course.id, self.old_course.location)
self.course = modulestore('split').get_course(locator)
@patch("sys.stdout", new_callable=StringIO)
def test_happy_path(self, mock_stdout):
locator = self.course.location
call_command(
"rollback_split_course",
str(locator),
)
with self.assertRaises(ItemNotFoundError):
modulestore('split').get_course(locator)
self.assertIn("Course rolled back successfully", mock_stdout.getvalue())
......@@ -133,7 +133,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
locator = loc_mapper().translate_location(course.location.course_id, descriptor.location, False, True)
locator = loc_mapper().translate_location(course.location.course_id, descriptor.location, True, True)
resp = self.client.get_html(locator.url_reverse('unit'))
self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
......@@ -144,12 +144,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_advanced_components_in_edit_unit(self):
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
# response HTML
self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Word cloud',
'Annotation',
'Text Annotation',
'Video Annotation',
'Open Response Assessment',
'Peer Grading Interface'])
self.check_components_on_page(
ADVANCED_COMPONENT_TYPES,
['Word cloud', 'Annotation', 'Text Annotation', 'Video Annotation',
'Open Response Assessment', 'Peer Grading Interface'],
)
def test_advanced_components_require_two_clicks(self):
self.check_components_on_page(['word_cloud'], ['Word cloud'])
......@@ -161,7 +160,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
location = descriptor.location.replace(name='.' + descriptor.location.name)
locator = loc_mapper().translate_location(course_items[0].location.course_id, location, False, True)
locator = loc_mapper().translate_location(
course_items[0].location.course_id, location, add_entry_if_missing=True)
resp = self.client.get_html(locator.url_reverse('unit'))
self.assertEqual(resp.status_code, 400)
......@@ -449,7 +449,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
""" Returns the locator for a given tab. """
tab_location = 'i4x://edX/999/static_tab/{0}'.format(tab['url_slug'])
return loc_mapper().translate_location(
course.location.course_id, Location(tab_location), False, True
course.location.course_id, Location(tab_location), True, True
)
def _create_static_tabs(self):
......@@ -457,7 +457,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
module_store = modulestore('direct')
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
course_location = Location('i4x', 'edX', '999', 'course', 'Robot_Super_Course', None)
new_location = loc_mapper().translate_location(course_location.course_id, course_location, False, True)
new_location = loc_mapper().translate_location(course_location.course_id, course_location, True, True)
ItemFactory.create(
parent_location=course_location,
......@@ -512,7 +512,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# also try a custom response which will trigger the 'is this course in whitelist' logic
locator = loc_mapper().translate_location(
course_items[0].location.course_id, location, False, True
course_items[0].location.course_id, location, True, True
)
resp = self.client.get_html(locator.url_reverse('xblock'))
self.assertEqual(resp.status_code, 200)
......@@ -534,7 +534,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent points to the child object which is to be deleted
self.assertTrue(sequential.location.url() in chapter.children)
location = loc_mapper().translate_location(course_location.course_id, sequential.location, False, True)
location = loc_mapper().translate_location(course_location.course_id, sequential.location, True, True)
self.client.delete(location.url_reverse('xblock'), {'recurse': True, 'all_versions': True})
found = False
......@@ -685,7 +685,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view
course = course_items[0]
location = loc_mapper().translate_location(course.location.course_id, course.location, False, True)
location = loc_mapper().translate_location(course.location.course_id, course.location, True, True)
url = location.url_reverse('assets/', '/c4x/edX/toy/asset/sample_static.txt')
resp = self.client.delete(url)
self.assertEqual(resp.status_code, 204)
......@@ -1062,7 +1062,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
)
# Unit test fails in Jenkins without this.
loc_mapper().translate_location(course_location.course_id, course_location, False, True)
loc_mapper().translate_location(course_location.course_id, course_location, True, True)
items = module_store.get_items(stub_location.replace(category='vertical', name=None))
self._check_verticals(items, course_location.course_id)
......@@ -1353,7 +1353,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# Assert is here to make sure that the course being tested actually has verticals (units) to check.
self.assertGreater(len(items), 0)
for descriptor in items:
unit_locator = loc_mapper().translate_location(course_id, descriptor.location, False, True)
unit_locator = loc_mapper().translate_location(course_id, descriptor.location, True, True)
resp = self.client.get_html(unit_locator.url_reverse('unit'))
self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
......@@ -1645,7 +1645,7 @@ class ContentStoreTest(ModuleStoreTestCase):
import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None])
new_location = loc_mapper().translate_location(loc.course_id, loc, False, True)
new_location = loc_mapper().translate_location(loc.course_id, loc, True, True)
resp = self._show_course_overview(loc)
self.assertEqual(resp.status_code, 200)
......@@ -1666,14 +1666,14 @@ class ContentStoreTest(ModuleStoreTestCase):
# go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence')
subsection_locator = loc_mapper().translate_location(loc.course_id, subsection_location, False, True)
subsection_locator = loc_mapper().translate_location(loc.course_id, subsection_location, True, True)
resp = self.client.get_html(subsection_locator.url_reverse('subsection'))
self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
# go look at the Edit page
unit_location = loc.replace(category='vertical', name='test_vertical')
unit_locator = loc_mapper().translate_location(loc.course_id, unit_location, False, True)
unit_locator = loc_mapper().translate_location(loc.course_id, unit_location, True, True)
resp = self.client.get_html(unit_locator.url_reverse('unit'))
self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
......@@ -1681,7 +1681,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def delete_item(category, name):
""" Helper method for testing the deletion of an xblock item. """
del_loc = loc.replace(category=category, name=name)
del_location = loc_mapper().translate_location(loc.course_id, del_loc, False, True)
del_location = loc_mapper().translate_location(loc.course_id, del_loc, True, True)
resp = self.client.delete(del_location.url_reverse('xblock'))
self.assertEqual(resp.status_code, 204)
_test_no_locations(self, resp, status_code=204, html=False)
......@@ -1883,7 +1883,7 @@ class ContentStoreTest(ModuleStoreTestCase):
"""
Show the course overview page.
"""
new_location = loc_mapper().translate_location(location.course_id, location, False, True)
new_location = loc_mapper().translate_location(location.course_id, location, True, True)
resp = self.client.get_html(new_location.url_reverse('course/', ''))
_test_no_locations(self, resp)
return resp
......@@ -1998,7 +1998,7 @@ def _course_factory_create_course():
Creates a course via the CourseFactory and returns the locator for it.
"""
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
return loc_mapper().translate_location(course.location.course_id, course.location, False, True)
return loc_mapper().translate_location(course.location.course_id, course.location, True, True)
def _get_course_id(test_course_data):
......
......@@ -152,6 +152,9 @@ def clear_existing_modulestores():
_MODULESTORES.clear()
# pylint: disable=W0603
global _loc_singleton
cache = getattr(_loc_singleton, "cache", None)
if cache:
cache.clear()
_loc_singleton = None
......
......@@ -219,6 +219,11 @@ class LocMapperStore(object):
return None
result = None
for candidate in maps:
if get_course and 'name' in candidate['_id']:
candidate_id = candidate['_id']
return Location(
'i4x', candidate_id['org'], candidate_id['course'], 'course', candidate_id['name']
)
old_course_id = self._generate_location_course_id(candidate['_id'])
for old_name, cat_to_usage in candidate['block_map'].iteritems():
for category, block_id in cat_to_usage.iteritems():
......@@ -261,8 +266,6 @@ class LocMapperStore(object):
return cached
location_id = self._interpret_location_course_id(old_style_course_id, location)
if old_style_course_id is None:
old_style_course_id = self._generate_location_course_id(location_id)
maps = self.location_map.find(location_id)
maps = list(maps)
......@@ -323,7 +326,7 @@ class LocMapperStore(object):
def _generate_location_course_id(self, entry_id):
"""
Generate a Location course_id for the given entry's id
Generate a Location course_id for the given entry's id.
"""
# strip id envelope if any
entry_id = entry_id.get('_id', entry_id)
......@@ -401,6 +404,8 @@ class LocMapperStore(object):
"""
Get the course Locator for this old course id
"""
if not old_course_id:
return None
entry = self.cache.get(old_course_id)
if entry is not None:
if published:
......@@ -425,6 +430,8 @@ class LocMapperStore(object):
"""
For quick lookup of courses
"""
if not old_course_id:
return
self.cache.set(old_course_id, (published_course_locator, draft_course_locator))
def _cache_location_map_entry(self, old_course_id, location, published_usage, draft_usage):
......
......@@ -23,7 +23,7 @@ class SplitMigrator(object):
self.draft_modulestore = draft_modulestore
self.loc_mapper = loc_mapper
def migrate_mongo_course(self, course_location, user_id, new_package_id=None):
def migrate_mongo_course(self, course_location, user, new_package_id=None):
"""
Create a new course in split_mongo representing the published and draft versions of the course from the
original mongo store. And return the new_package_id (which the caller can also get by calling
......@@ -32,7 +32,7 @@ class SplitMigrator(object):
If the new course already exists, this raises DuplicateItemError
:param course_location: a Location whose category is 'course' and points to the course
:param user_id: the user whose action is causing this migration
:param user: the user whose action is causing this migration
:param new_package_id: (optional) the Locator.package_id for the new course. Defaults to
whatever translate_location_to_locator returns
"""
......@@ -48,18 +48,18 @@ class SplitMigrator(object):
new_course_root_locator = self.loc_mapper.translate_location(old_course_id, course_location)
new_course = self.split_modulestore.create_course(
course_location.org, original_course.display_name,
user_id, id_root=new_package_id,
user.id, id_root=new_package_id,
fields=self._get_json_fields_translate_children(original_course, old_course_id, True),
root_block_id=new_course_root_locator.block_id,
master_branch=new_course_root_locator.branch
)
self._copy_published_modules_to_course(new_course, course_location, old_course_id, user_id)
self._add_draft_modules_to_course(new_package_id, old_course_id, course_location, user_id)
self._copy_published_modules_to_course(new_course, course_location, old_course_id, user)
self._add_draft_modules_to_course(new_package_id, old_course_id, course_location, user)
return new_package_id
def _copy_published_modules_to_course(self, new_course, old_course_loc, old_course_id, user_id):
def _copy_published_modules_to_course(self, new_course, old_course_loc, old_course_id, user):
"""
Copy all of the modules from the 'direct' version of the course to the new split course.
"""
......@@ -79,7 +79,7 @@ class SplitMigrator(object):
old_course_id, module.location, True, add_entry_if_missing=True
)
_new_module = self.split_modulestore.create_item(
course_version_locator, module.category, user_id,
course_version_locator, module.category, user.id,
block_id=new_locator.block_id,
fields=self._get_json_fields_translate_children(module, old_course_id, True),
continue_version=True
......@@ -94,7 +94,7 @@ class SplitMigrator(object):
# children which meant some pointers were to non-existent locations in 'direct'
self.split_modulestore.internal_clean_children(course_version_locator)
def _add_draft_modules_to_course(self, new_package_id, old_course_id, old_course_loc, user_id):
def _add_draft_modules_to_course(self, new_package_id, old_course_id, old_course_loc, user):
"""
update each draft. Create any which don't exist in published and attach to their parents.
"""
......@@ -124,12 +124,12 @@ class SplitMigrator(object):
if name != 'children' and field.is_set_on(module):
field.write_to(split_module, field.read_from(module))
_new_module = self.split_modulestore.update_item(split_module, user_id)
_new_module = self.split_modulestore.update_item(split_module, user.id)
else:
# only a draft version (aka, 'private'). parent needs updated too.
# create a new course version just in case the current head is also the prod head
_new_module = self.split_modulestore.create_item(
new_draft_course_loc, module.category, user_id,
new_draft_course_loc, module.category, user.id,
block_id=new_locator.block_id,
fields=self._get_json_fields_translate_children(module, old_course_id, True)
)
......@@ -156,7 +156,7 @@ class SplitMigrator(object):
new_parent_cursor = idx + 1
break
new_parent.children.insert(new_parent_cursor, new_block_id)
new_parent = self.split_modulestore.update_item(new_parent, user_id)
new_parent = self.split_modulestore.update_item(new_parent, user.id)
def _get_json_fields_translate_children(self, xblock, old_course_id, published):
"""
......
......@@ -1284,6 +1284,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if index is None:
raise ItemNotFoundError(package_id)
# this is the only real delete in the system. should it do something else?
log.info("deleting course from split-mongo: %s", package_id)
self.db_connection.delete_course_index(index['_id'])
def get_errored_courses(self):
......
......@@ -4,8 +4,8 @@ Modulestore configuration for test cases.
from uuid import uuid4
from django.test import TestCase
from xmodule.modulestore.django import editable_modulestore, \
clear_existing_modulestores
from xmodule.modulestore.django import (
editable_modulestore, clear_existing_modulestores, loc_mapper)
from xmodule.contentstore.django import contentstore
......@@ -225,6 +225,9 @@ class ModuleStoreTestCase(TestCase):
if contentstore().fs_files:
db = contentstore().fs_files.database
db.connection.drop_database(db)
location_mapper = loc_mapper()
if location_mapper.db:
location_mapper.location_map.drop()
@classmethod
def setUpClass(cls):
......
......@@ -271,7 +271,8 @@ class TestMigration(unittest.TestCase):
self.compare_dags(presplit, pre_child, split_child, published)
def test_migrator(self):
self.migrator.migrate_mongo_course(self.course_location, random.getrandbits(32))
user = mock.Mock(id=1)
self.migrator.migrate_mongo_course(self.course_location, user)
# now compare the migrated to the original course
self.compare_courses(self.old_mongo, True)
self.compare_courses(self.draft_mongo, False)
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