コード例 #1
0
    def delete_item(self, location, user_id, revision=None, **kwargs):
        """
        Delete the given item from persistence. kwargs allow modulestore specific parameters.

        Args:
            location: UsageKey of the item to be deleted
            user_id: id of the user deleting the item
            revision:
                None - deletes the item and its subtree, and updates the parents per description above
                ModuleStoreEnum.RevisionOption.published_only - removes only Published versions
                ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents
                    currently only provided by contentstore.views.item.orphan_handler
                Otherwise, raises a ValueError.
        """
        with self.bulk_operations(location.course_key):
            if revision == ModuleStoreEnum.RevisionOption.published_only:
                branches_to_delete = [ModuleStoreEnum.BranchName.published]
            elif revision == ModuleStoreEnum.RevisionOption.all:
                branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
            elif revision is None:
                branches_to_delete = [ModuleStoreEnum.BranchName.draft]
            else:
                raise UnsupportedRevisionError(
                    [
                        None,
                        ModuleStoreEnum.RevisionOption.published_only,
                        ModuleStoreEnum.RevisionOption.all
                    ]
                )

            for branch in branches_to_delete:
                branched_location = location.for_branch(branch)
                parent_loc = self.get_parent_location(branched_location)
                SplitMongoModuleStore.delete_item(self, branched_location, user_id)
                self._auto_publish_no_children(parent_loc, parent_loc.category, user_id, **kwargs)
コード例 #2
0
    def delete_item(self, location, user_id, revision=None, **kwargs):
        """
        Delete the given item from persistence. kwargs allow modulestore specific parameters.

        Args:
            location: UsageKey of the item to be deleted
            user_id: id of the user deleting the item
            revision:
                None - deletes the item and its subtree, and updates the parents per description above
                ModuleStoreEnum.RevisionOption.published_only - removes only Published versions
                ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents
                    currently only provided by contentstore.views.item.orphan_handler
                Otherwise, raises a ValueError.
        """
        with self.bulk_operations(location.course_key):
            if revision == ModuleStoreEnum.RevisionOption.published_only:
                branches_to_delete = [ModuleStoreEnum.BranchName.published]
            elif revision == ModuleStoreEnum.RevisionOption.all:
                branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
            elif revision is None:
                branches_to_delete = [ModuleStoreEnum.BranchName.draft]
            else:
                raise UnsupportedRevisionError(
                    [
                        None,
                        ModuleStoreEnum.RevisionOption.published_only,
                        ModuleStoreEnum.RevisionOption.all
                    ]
                )

            for branch in branches_to_delete:
                branched_location = location.for_branch(branch)
                parent_loc = self.get_parent_location(branched_location)
                SplitMongoModuleStore.delete_item(self, branched_location, user_id)
                self._auto_publish_no_children(parent_loc, parent_loc.category, user_id, **kwargs)
コード例 #3
0
    def delete_item(self, location, user_id, revision=None, skip_auto_publish=False, **kwargs):
        """
        Delete the given item from persistence. kwargs allow modulestore specific parameters.

        Args:
            location: UsageKey of the item to be deleted
            user_id: id of the user deleting the item
            revision:
                None - deletes the item and its subtree, and updates the parents per description above
                ModuleStoreEnum.RevisionOption.published_only - removes only Published versions
                ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents
                    currently only provided by contentstore.views.item.orphan_handler
                Otherwise, raises a ValueError.
        """
        with self.bulk_operations(location.course_key):
            if isinstance(location, LibraryUsageLocator):
                branches_to_delete = [ModuleStoreEnum.BranchName.library]  # Libraries don't yet have draft/publish support
            elif revision == ModuleStoreEnum.RevisionOption.published_only:
                branches_to_delete = [ModuleStoreEnum.BranchName.published]
            elif revision == ModuleStoreEnum.RevisionOption.all:
                branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
            elif revision is None:
                branches_to_delete = [ModuleStoreEnum.BranchName.draft]
                draft_location = location.for_branch(ModuleStoreEnum.BranchName.draft)
                try:
                    item = self.get_item(draft_location)
                    if getattr(item, 'has_children', False):
                        # If item have has_published_version then delete published children also.
                        if self.has_published_version(item):
                            branches_to_delete.insert(0, ModuleStoreEnum.BranchName.published)
                except ItemNotFoundError:
                    # Raises ValueError as in function description
                    raise ValueError("Cannot delete a block that does not exist")
            else:
                raise UnsupportedRevisionError(
                    [
                        None,
                        ModuleStoreEnum.RevisionOption.published_only,
                        ModuleStoreEnum.RevisionOption.all
                    ]
                )

            self._flag_publish_event(location.course_key)
            for branch in branches_to_delete:
                branched_location = location.for_branch(branch)
                parent_loc = self.get_parent_location(branched_location)
                SplitMongoModuleStore.delete_item(self, branched_location, user_id)
                # publish parent w/o child if deleted element is direct only (not based on type of parent)
                # publish vertical to behave more like the old mongo/draft modulestore - TNL-2593
                if (
                        branch == ModuleStoreEnum.BranchName.draft and
                        branched_location.block_type in (DIRECT_ONLY_CATEGORIES + ['vertical']) and
                        parent_loc and
                        not skip_auto_publish
                ):
                    # will publish if its not an orphan
                    self.publish(parent_loc.version_agnostic(), user_id, blacklist=EXCLUDE_ALL, **kwargs)
コード例 #4
0
 def setUp(self):
     super(TestMigration, self).setUp()
     self.loc_mapper = LocMapperStore(**self.db_config)
     self.old_mongo = MongoModuleStore(**self.modulestore_options)
     self.draft_mongo = DraftModuleStore(**self.modulestore_options)
     self.split_mongo = SplitMongoModuleStore(
         loc_mapper=self.loc_mapper, **self.modulestore_options
     )
     self.migrator = SplitMigrator(self.split_mongo, self.old_mongo, self.draft_mongo, self.loc_mapper)
     self.course_location = None
     self.create_source_course()
コード例 #5
0
    def delete_item(self, location, user_id, revision=None, **kwargs):
        """
        Delete the given item from persistence. kwargs allow modulestore specific parameters.

        Args:
            location: UsageKey of the item to be deleted
            user_id: id of the user deleting the item
            revision:
                None - deletes the item and its subtree, and updates the parents per description above
                ModuleStoreEnum.RevisionOption.published_only - removes only Published versions
                ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents
                    currently only provided by contentstore.views.item.orphan_handler
                Otherwise, raises a ValueError.
        """
        with self.bulk_operations(location.course_key):
            if isinstance(location, LibraryUsageLocator):
                branches_to_delete = [
                    ModuleStoreEnum.BranchName.library
                ]  # Libraries don't yet have draft/publish support
            elif revision == ModuleStoreEnum.RevisionOption.published_only:
                branches_to_delete = [ModuleStoreEnum.BranchName.published]
            elif revision == ModuleStoreEnum.RevisionOption.all:
                branches_to_delete = [
                    ModuleStoreEnum.BranchName.published,
                    ModuleStoreEnum.BranchName.draft
                ]
            elif revision is None:
                branches_to_delete = [ModuleStoreEnum.BranchName.draft]
            else:
                raise UnsupportedRevisionError([
                    None, ModuleStoreEnum.RevisionOption.published_only,
                    ModuleStoreEnum.RevisionOption.all
                ])

            for branch in branches_to_delete:
                branched_location = location.for_branch(branch)
                parent_loc = self.get_parent_location(branched_location)
                SplitMongoModuleStore.delete_item(self, branched_location,
                                                  user_id)
                # publish parent w/o child if deleted element is direct only (not based on type of parent)
                if branch == ModuleStoreEnum.BranchName.draft and branched_location.block_type in DIRECT_ONLY_CATEGORIES:
                    self.publish(parent_loc.version_agnostic(),
                                 user_id,
                                 blacklist=EXCLUDE_ALL,
                                 **kwargs)

        # Remove this location from the courseware search index so that searches
        # will refrain from showing it as a result
        CoursewareSearchIndexer.add_to_search_index(self,
                                                    location,
                                                    delete=True)
コード例 #6
0
 def get_library(self, library_id, depth=0, head_validation=True, **kwargs):
     if not head_validation and library_id.version_guid:
         return SplitMongoModuleStore.get_library(
             self, library_id, depth=depth, head_validation=head_validation, **kwargs
         )
     library_id = self._map_revision_to_branch(library_id)
     return super().get_library(library_id, depth=depth, **kwargs)
コード例 #7
0
 def setUp(self):
     self.user_id = random.getrandbits(32)
     super(SplitWMongoCourseBootstrapper, self).setUp()
     self.split_mongo = SplitMongoModuleStore(None, self.db_config,
                                              **self.modulestore_options)
     self.addCleanup(self.split_mongo._drop_database)  # pylint: disable=protected-access
     self.draft_mongo = DraftMongoModuleStore(
         None,
         self.db_config,
         branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
         metadata_inheritance_cache_subsystem=MemoryCache(),
         **self.modulestore_options)
     self.addCleanup(self.draft_mongo._drop_database)  # pylint: disable=protected-access
     self.old_course_key = None
     self.runtime = None
     self._create_course()
コード例 #8
0
 def get_library(self, library_id, depth=0, head_validation=True, **kwargs):
     if not head_validation and library_id.version_guid:
         return SplitMongoModuleStore.get_library(
             self, library_id, depth=depth, head_validation=head_validation, **kwargs
         )
     library_id = self._map_revision_to_branch(library_id)
     return super(DraftVersioningModuleStore, self).get_library(library_id, depth=depth, **kwargs)
コード例 #9
0
    def setUp(self):
        self.db_config['collection'] = 'modulestore{0}'.format(uuid.uuid4().hex[:5])

        self.userid = random.getrandbits(32)
        super(SplitWMongoCourseBoostrapper, self).setUp()
        self.split_mongo = SplitMongoModuleStore(
            self.db_config,
            **self.modulestore_options
        )
        self.addCleanup(self.split_mongo.db.connection.close)
        self.addCleanup(self.tear_down_split)
        self.old_mongo = MongoModuleStore(self.db_config, **self.modulestore_options)
        self.draft_mongo = DraftMongoModuleStore(self.db_config, **self.modulestore_options)
        self.addCleanup(self.tear_down_mongo)
        self.old_course_key = None
        self.runtime = None
        self._create_course()
コード例 #10
0
 def setUp(self):
     super(TestMigration, self).setUp()
     # pylint: disable=W0142
     self.loc_mapper = LocMapperStore(test_location_mapper.TrivialCache(),
                                      **self.db_config)
     self.old_mongo = MongoModuleStore(self.db_config,
                                       **self.modulestore_options)
     self.draft_mongo = DraftModuleStore(self.db_config,
                                         **self.modulestore_options)
     self.split_mongo = SplitMongoModuleStore(
         doc_store_config=self.db_config,
         loc_mapper=self.loc_mapper,
         **self.modulestore_options)
     self.migrator = SplitMigrator(self.split_mongo, self.old_mongo,
                                   self.draft_mongo, self.loc_mapper)
     self.course_location = None
     self.create_source_course()
コード例 #11
0
 def setUp(self):
     super(TestMigration, self).setUp()
     noop_cache = mock.Mock(spec=['get', 'set_many'])
     noop_cache.configure_mock(**{'get.return_value': None})
     # pylint: disable=W0142
     self.loc_mapper = LocMapperStore(noop_cache, **self.db_config)
     self.old_mongo = MongoModuleStore(self.db_config,
                                       **self.modulestore_options)
     self.draft_mongo = DraftModuleStore(self.db_config,
                                         **self.modulestore_options)
     self.split_mongo = SplitMongoModuleStore(
         doc_store_config=self.db_config,
         loc_mapper=self.loc_mapper,
         **self.modulestore_options)
     self.migrator = SplitMigrator(self.split_mongo, self.old_mongo,
                                   self.draft_mongo, self.loc_mapper)
     self.course_location = None
     self.create_source_course()
コード例 #12
0
ファイル: split_draft.py プロジェクト: Taxellool/edx-platform
    def delete_item(self, location, user_id, revision=None, **kwargs):
        """
        Delete the given item from persistence. kwargs allow modulestore specific parameters.

        Args:
            location: UsageKey of the item to be deleted
            user_id: id of the user deleting the item
            revision:
                None - deletes the item and its subtree, and updates the parents per description above
                ModuleStoreEnum.RevisionOption.published_only - removes only Published versions
                ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents
                    currently only provided by contentstore.views.item.orphan_handler
                Otherwise, raises a ValueError.
        """
        with self.bulk_operations(location.course_key):
            if isinstance(location, LibraryUsageLocator):
                branches_to_delete = [ModuleStoreEnum.BranchName.library]  # Libraries don't yet have draft/publish support
            elif revision == ModuleStoreEnum.RevisionOption.published_only:
                branches_to_delete = [ModuleStoreEnum.BranchName.published]
            elif revision == ModuleStoreEnum.RevisionOption.all:
                branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
            elif revision is None:
                branches_to_delete = [ModuleStoreEnum.BranchName.draft]
            else:
                raise UnsupportedRevisionError(
                    [
                        None,
                        ModuleStoreEnum.RevisionOption.published_only,
                        ModuleStoreEnum.RevisionOption.all
                    ]
                )

            self._flag_publish_event(location.course_key)
            for branch in branches_to_delete:
                branched_location = location.for_branch(branch)
                parent_loc = self.get_parent_location(branched_location)
                SplitMongoModuleStore.delete_item(self, branched_location, user_id)
                # publish parent w/o child if deleted element is direct only (not based on type of parent)
                if branch == ModuleStoreEnum.BranchName.draft and branched_location.block_type in DIRECT_ONLY_CATEGORIES:
                    self.publish(parent_loc.version_agnostic(), user_id, blacklist=EXCLUDE_ALL, **kwargs)

        # Remove this location from the courseware search index so that searches
        # will refrain from showing it as a result
        CoursewareSearchIndexer.add_to_search_index(self, location, delete=True)
コード例 #13
0
    def setUp(self):
        self.db_config['collection'] = 'modulestore{0}'.format(
            uuid.uuid4().hex[:5])

        self.user_id = random.getrandbits(32)
        super(SplitWMongoCourseBoostrapper, self).setUp()
        self.split_mongo = SplitMongoModuleStore(None, self.db_config,
                                                 **self.modulestore_options)
        self.addCleanup(self.split_mongo.db.connection.close)
        self.addCleanup(self.tear_down_split)
        self.draft_mongo = DraftMongoModuleStore(
            None,
            self.db_config,
            branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
            metadata_inheritance_cache_subsystem=MemoryCache(),
            **self.modulestore_options)
        self.addCleanup(self.tear_down_mongo)
        self.old_course_key = None
        self.runtime = None
        self._create_course()
コード例 #14
0
 def get_library(self, library_id, depth=0, head_validation=True, **kwargs):
     if not head_validation and library_id.version_guid:
         return SplitMongoModuleStore.get_library(
             self,
             library_id,
             depth=depth,
             head_validation=head_validation,
             **kwargs)
     library_id = self._map_revision_to_branch(library_id)
     return super(DraftVersioningModuleStore, self).get_library(library_id,
                                                                depth=depth,
                                                                **kwargs)  # lint-amnesty, pylint: disable=super-with-arguments
コード例 #15
0
 def setUp(self):
     super(TestMigration, self).setUp()
     # pylint: disable=W0142
     self.loc_mapper = LocMapperStore(test_location_mapper.TrivialCache(), **self.db_config)
     self.old_mongo = MongoModuleStore(self.db_config, **self.modulestore_options)
     self.draft_mongo = DraftModuleStore(self.db_config, **self.modulestore_options)
     self.split_mongo = SplitMongoModuleStore(
         doc_store_config=self.db_config, loc_mapper=self.loc_mapper, **self.modulestore_options
     )
     self.migrator = SplitMigrator(self.split_mongo, self.old_mongo, self.draft_mongo, self.loc_mapper)
     self.course_location = None
     self.create_source_course()
コード例 #16
0
 def setUp(self):
     super(TestMigration, self).setUp()
     noop_cache = mock.Mock(spec=['get', 'set_many'])
     noop_cache.configure_mock(**{'get.return_value': None})
     # pylint: disable=W0142
     self.loc_mapper = LocMapperStore(noop_cache, **self.db_config)
     self.old_mongo = MongoModuleStore(self.db_config, **self.modulestore_options)
     self.draft_mongo = DraftModuleStore(self.db_config, **self.modulestore_options)
     self.split_mongo = SplitMongoModuleStore(
         doc_store_config=self.db_config,
         loc_mapper=self.loc_mapper,
         **self.modulestore_options
     )
     self.migrator = SplitMigrator(self.split_mongo, self.old_mongo, self.draft_mongo, self.loc_mapper)
     self.course_location = None
     self.create_source_course()
コード例 #17
0
 def setUp(self):
     self.user_id = random.getrandbits(32)
     super(SplitWMongoCourseBootstrapper, self).setUp()
     self.split_mongo = SplitMongoModuleStore(
         None,
         self.db_config,
         **self.modulestore_options
     )
     self.addCleanup(self.split_mongo._drop_database)  # pylint: disable=protected-access
     self.draft_mongo = DraftMongoModuleStore(
         None, self.db_config, branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
         metadata_inheritance_cache_subsystem=MemoryCache(),
         **self.modulestore_options
     )
     self.addCleanup(self.draft_mongo._drop_database)  # pylint: disable=protected-access
     self.old_course_key = None
     self.runtime = None
     self._create_course()
コード例 #18
0
    def setUp(self):
        self.db_config['collection'] = 'modulestore{0}'.format(uuid.uuid4().hex[:5])

        self.user_id = random.getrandbits(32)
        super(SplitWMongoCourseBoostrapper, self).setUp()
        self.split_mongo = SplitMongoModuleStore(
            None,
            self.db_config,
            **self.modulestore_options
        )
        self.addCleanup(self.split_mongo.db.connection.close)
        self.addCleanup(self.tear_down_split)
        self.draft_mongo = DraftMongoModuleStore(
            None, self.db_config, branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
            metadata_inheritance_cache_subsystem=MemoryCache(),
            **self.modulestore_options
        )
        self.addCleanup(self.tear_down_mongo)
        self.old_course_key = None
        self.runtime = None
        self._create_course()
    def build(self, contentstore):
        """
        A contextmanager that returns an isolated versioning modulestore, and then deletes
        all of its data at the end of the context.

        Args:
            contentstore: The contentstore that this modulestore should use to store
                all of its assets.
        """
        # pylint: disable=unreachable
        raise SkipTest(
            "DraftVersioningModuleStore doesn't yet support the same interface as the rest of the modulestores"
        )
        doc_store_config = dict(db='modulestore{}'.format(
            random.randint(0, 10000)),
                                collection='split_module',
                                **COMMON_DOCSTORE_CONFIG)
        # Set up a temp directory for storing filesystem content created during import
        fs_root = mkdtemp()

        modulestore = SplitMongoModuleStore(
            contentstore,
            doc_store_config,
            fs_root,
            render_template=repr,
        )

        try:
            yield modulestore
        finally:
            # Delete the created database
            db = modulestore.db
            db.connection.drop_database(db)
            db.connection.close()

            # Delete the created directory on the filesystem
            rmtree(fs_root)
コード例 #20
0
class SplitWMongoCourseBootstrapper(unittest.TestCase):
    """
    Helper for tests which need to construct split mongo & old mongo based courses to get interesting internal structure.
    Override _create_course and after invoking the super() _create_course, have it call _create_item for
    each xblock you want in the course.
    This class ensures the db gets created, opened, and cleaned up in addition to creating the course

    Defines the following attrs on self:
    * user_id: a random non-registered mock user id
    * split_mongo: a pointer to the split mongo instance
    * draft_mongo: a pointer to the old draft instance
    * split_course_key (CourseLocator): of the new course
    * old_course_key: the SlashSpecifiedCourseKey for the course
    """
    # Snippet of what would be in the django settings envs file
    db_config = {
        'host': MONGO_HOST,
        'port': MONGO_PORT_NUM,
        'db': 'test_xmodule_{}'.format(os.getpid()),
        'collection': 'modulestore'
    }

    modulestore_options = {
        'default_class': 'xmodule.raw_module.RawDescriptor',
        'fs_root': '',
        'render_template': mock.Mock(return_value=""),
        'xblock_mixins': (InheritanceMixin, XModuleMixin)
    }

    split_course_key = CourseLocator('test_org',
                                     'test_course',
                                     'runid',
                                     branch=ModuleStoreEnum.BranchName.draft)

    def setUp(self):
        self.user_id = random.getrandbits(32)
        super(SplitWMongoCourseBootstrapper, self).setUp()
        self.split_mongo = SplitMongoModuleStore(None, self.db_config,
                                                 **self.modulestore_options)
        self.addCleanup(self.split_mongo._drop_database)  # pylint: disable=protected-access
        self.draft_mongo = DraftMongoModuleStore(
            None,
            self.db_config,
            branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
            metadata_inheritance_cache_subsystem=MemoryCache(),
            **self.modulestore_options)
        self.addCleanup(self.draft_mongo._drop_database)  # pylint: disable=protected-access
        self.old_course_key = None
        self.runtime = None
        self._create_course()

    def _create_item(self,
                     category,
                     name,
                     data,
                     metadata,
                     parent_category,
                     parent_name,
                     draft=True,
                     split=True):
        """
        Create the item of the given category and block id in split and old mongo, add it to the optional
        parent. The parent category is only needed because old mongo requires it for the id.

        Note: if draft = False, it will create the draft and then publish it; so, it will overwrite any
        existing draft for both the new item and the parent
        """
        location = self.old_course_key.make_usage_key(category, name)
        self.draft_mongo.create_item(self.user_id,
                                     location.course_key,
                                     location.block_type,
                                     block_id=location.block_id,
                                     definition_data=data,
                                     metadata=metadata,
                                     runtime=self.runtime)
        if not draft:
            self.draft_mongo.publish(location, self.user_id)
        if isinstance(data, basestring):
            fields = {'data': data}
        else:
            fields = data.copy()
        fields.update(metadata)
        if parent_name:
            # add child to parent in mongo
            parent_location = self.old_course_key.make_usage_key(
                parent_category, parent_name)
            parent = self.draft_mongo.get_item(parent_location)
            parent.children.append(location)
            self.draft_mongo.update_item(parent, self.user_id)
            if not draft:
                self.draft_mongo.publish(parent_location, self.user_id)
            # create child for split
            if split:
                self.split_mongo.create_child(
                    self.user_id,
                    BlockUsageLocator(course_key=self.split_course_key,
                                      block_type=parent_category,
                                      block_id=parent_name),
                    category,
                    block_id=name,
                    fields=fields)
        else:
            if split:
                self.split_mongo.create_item(self.user_id,
                                             self.split_course_key,
                                             category,
                                             block_id=name,
                                             fields=fields)

    def _create_course(self, split=True):
        """
        * some detached items
        * some attached children
        * some orphans
        """
        metadata = {
            'start': datetime.datetime(2000, 3, 13, 4),
            'display_name': 'Migration test course',
        }
        data = {'wiki_slug': 'test_course_slug'}
        fields = metadata.copy()
        fields.update(data)
        if split:
            # split requires the course to be created separately from creating items
            self.split_mongo.create_course(self.split_course_key.org,
                                           self.split_course_key.course,
                                           self.split_course_key.run,
                                           self.user_id,
                                           fields=fields,
                                           root_block_id='runid')
        old_course = self.draft_mongo.create_course(self.split_course_key.org,
                                                    'test_course',
                                                    'runid',
                                                    self.user_id,
                                                    fields=fields)
        self.old_course_key = old_course.id
        self.runtime = old_course.runtime
コード例 #21
0
class SplitWMongoCourseBoostrapper(unittest.TestCase):
    """
    Helper for tests which need to construct split mongo & old mongo based courses to get interesting internal structure.
    Override _create_course and after invoking the super() _create_course, have it call _create_item for
    each xblock you want in the course.
    This class ensures the db gets created, opened, and cleaned up in addition to creating the course

    Defines the following attrs on self:
    * userid: a random non-registered mock user id
    * split_mongo: a pointer to the split mongo instance
    * old_mongo: a pointer to the old_mongo instance
    * draft_mongo: a pointer to the old draft instance
    * split_course_key (CourseLocator): of the new course
    * old_course_key: the SlashSpecifiedCourseKey for the course
    """
    # Snippet of what would be in the django settings envs file
    db_config = {
        'host': 'localhost',
        'db': 'test_xmodule',
    }

    modulestore_options = {
        'default_class': 'xmodule.raw_module.RawDescriptor',
        'fs_root': '',
        'render_template': mock.Mock(return_value=""),
        'xblock_mixins': (InheritanceMixin, )
    }

    split_course_key = CourseLocator('test_org',
                                     'test_course.runid',
                                     branch='draft')

    def setUp(self):
        self.db_config['collection'] = 'modulestore{0}'.format(
            uuid.uuid4().hex[:5])

        self.userid = random.getrandbits(32)
        super(SplitWMongoCourseBoostrapper, self).setUp()
        self.split_mongo = SplitMongoModuleStore(self.db_config,
                                                 **self.modulestore_options)
        self.addCleanup(self.split_mongo.db.connection.close)
        self.addCleanup(self.tear_down_split)
        self.old_mongo = MongoModuleStore(self.db_config,
                                          **self.modulestore_options)
        self.draft_mongo = DraftMongoModuleStore(self.db_config,
                                                 **self.modulestore_options)
        self.addCleanup(self.tear_down_mongo)
        self.old_course_key = None
        self.runtime = None
        self._create_course()

    def tear_down_split(self):
        """
        Remove the test collections, close the db connection
        """
        split_db = self.split_mongo.db
        split_db.drop_collection(split_db.course_index)
        split_db.drop_collection(split_db.structures)
        split_db.drop_collection(split_db.definitions)

    def tear_down_mongo(self):
        """
        Remove the test collections, close the db connection
        """
        split_db = self.split_mongo.db
        # old_mongo doesn't give a db attr, but all of the dbs are the same
        split_db.drop_collection(self.old_mongo.collection)

    def _create_item(self,
                     category,
                     name,
                     data,
                     metadata,
                     parent_category,
                     parent_name,
                     draft=True,
                     split=True):
        """
        Create the item of the given category and block id in split and old mongo, add it to the optional
        parent. The parent category is only needed because old mongo requires it for the id.
        """
        location = self.old_course_key.make_usage_key(category, name)
        if not draft or category in DIRECT_ONLY_CATEGORIES:
            mongo = self.old_mongo
        else:
            mongo = self.draft_mongo
        mongo.create_and_save_xmodule(location, data, metadata, self.runtime)
        if isinstance(data, basestring):
            fields = {'data': data}
        else:
            fields = data.copy()
        fields.update(metadata)
        if parent_name:
            # add child to parent in mongo
            parent_location = self.old_course_key.make_usage_key(
                parent_category, parent_name)
            if not draft or parent_category in DIRECT_ONLY_CATEGORIES:
                mongo = self.old_mongo
            else:
                mongo = self.draft_mongo
            parent = mongo.get_item(parent_location)
            parent.children.append(location)
            mongo.update_item(parent, self.userid)
            # create pointer for split
            course_or_parent_locator = BlockUsageLocator(
                course_key=self.split_course_key,
                block_type=parent_category,
                block_id=parent_name)
        else:
            course_or_parent_locator = self.split_course_key
        if split:
            self.split_mongo.create_item(course_or_parent_locator,
                                         category,
                                         self.userid,
                                         block_id=name,
                                         fields=fields)

    def _create_course(self, split=True):
        """
        * some detached items
        * some attached children
        * some orphans
        """
        metadata = {
            'start': datetime.datetime(2000, 3, 13, 4),
            'display_name': 'Migration test course',
        }
        data = {'wiki_slug': 'test_course_slug'}
        fields = metadata.copy()
        fields.update(data)
        if split:
            # split requires the course to be created separately from creating items
            self.split_mongo.create_course(self.split_course_key.org,
                                           self.split_course_key.offering,
                                           self.userid,
                                           fields=fields,
                                           root_block_id='runid')
        old_course = self.old_mongo.create_course(self.split_course_key.org,
                                                  'test_course/runid',
                                                  fields=fields)
        self.old_course_key = old_course.id
        self.runtime = old_course.runtime
コード例 #22
0
class TestMigration(unittest.TestCase):
    """
    Test the split migrator
    """

    # Snippet of what would be in the django settings envs file
    db_config = {
        'host': 'localhost',
        'db': 'test_xmodule',
        'collection': 'modulestore{0}'.format(uuid.uuid4().hex[:5]),
    }

    modulestore_options = {
        'default_class': 'xmodule.raw_module.RawDescriptor',
        'fs_root': '',
        'render_template': mock.Mock(return_value=""),
        'xblock_mixins': (InheritanceMixin,)
    }

    def setUp(self):
        super(TestMigration, self).setUp()
        self.loc_mapper = LocMapperStore(**self.db_config)
        self.old_mongo = MongoModuleStore(self.db_config, **self.modulestore_options)
        self.draft_mongo = DraftModuleStore(self.db_config, **self.modulestore_options)
        self.split_mongo = SplitMongoModuleStore(
            doc_store_config=self.db_config,
            loc_mapper=self.loc_mapper,
            **self.modulestore_options
        )
        self.migrator = SplitMigrator(self.split_mongo, self.old_mongo, self.draft_mongo, self.loc_mapper)
        self.course_location = None
        self.create_source_course()

    def tearDown(self):
        dbref = self.loc_mapper.db
        dbref.drop_collection(self.loc_mapper.location_map)
        split_db = self.split_mongo.db
        split_db.drop_collection(self.split_mongo.db_connection.course_index)
        split_db.drop_collection(self.split_mongo.db_connection.structures)
        split_db.drop_collection(self.split_mongo.db_connection.definitions)
        # old_mongo doesn't give a db attr, but all of the dbs are the same
        dbref.drop_collection(self.old_mongo.collection)

        dbref.connection.close()

        super(TestMigration, self).tearDown()

    def _create_and_get_item(self, store, location, data, metadata, runtime=None):
        store.create_and_save_xmodule(location, data, metadata, runtime)
        return store.get_item(location)

    def create_source_course(self):
        """
        A course testing all of the conversion mechanisms:
        * some inheritable settings
        * sequences w/ draft and live intermixed children to ensure all get to the draft but
        only the live ones get to published. Some are only draft, some are both, some are only live.
        * about, static_tab, and conditional documents
        """
        location = Location('i4x', 'test_org', 'test_course', 'course', 'runid')
        self.course_location = location
        date_proxy = Date()
        metadata = {
            'start': date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)),
            'display_name': 'Migration test course',
        }
        data = {
            'wiki_slug': 'test_course_slug'
        }
        course_root = self._create_and_get_item(self.old_mongo, location, data, metadata)
        runtime = course_root.runtime
        # chapters
        location = location.replace(category='chapter', name=uuid.uuid4().hex)
        chapter1 = self._create_and_get_item(self.old_mongo, location, {}, {'display_name': 'Chapter 1'}, runtime)
        course_root.children.append(chapter1.location.url())
        location = location.replace(category='chapter', name=uuid.uuid4().hex)
        chapter2 = self._create_and_get_item(self.old_mongo, location, {}, {'display_name': 'Chapter 2'}, runtime)
        course_root.children.append(chapter2.location.url())
        self.old_mongo.update_children(course_root.location, course_root.children)
        # vertical in live only
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        live_vert = self._create_and_get_item(self.old_mongo, location, {}, {'display_name': 'Live vertical'}, runtime)
        chapter1.children.append(live_vert.location.url())
        self.create_random_units(self.old_mongo, live_vert)
        # vertical in both live and draft
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        both_vert = self._create_and_get_item(
            self.old_mongo, location, {}, {'display_name': 'Both vertical'}, runtime
        )
        draft_both = self._create_and_get_item(
            self.draft_mongo, location, {}, {'display_name': 'Both vertical renamed'}, runtime
        )
        chapter1.children.append(both_vert.location.url())
        self.create_random_units(self.old_mongo, both_vert, self.draft_mongo, draft_both)
        # vertical in draft only (x2)
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        draft_vert = self._create_and_get_item(
            self.draft_mongo,
            location, {}, {'display_name': 'Draft vertical'}, runtime)
        chapter1.children.append(draft_vert.location.url())
        self.create_random_units(self.draft_mongo, draft_vert)
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        draft_vert = self._create_and_get_item(
            self.draft_mongo,
            location, {}, {'display_name': 'Draft vertical2'}, runtime)
        chapter1.children.append(draft_vert.location.url())
        self.create_random_units(self.draft_mongo, draft_vert)
        # and finally one in live only (so published has to skip 2)
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        live_vert = self._create_and_get_item(
            self.old_mongo,
            location, {}, {'display_name': 'Live vertical end'}, runtime)
        chapter1.children.append(live_vert.location.url())
        self.create_random_units(self.old_mongo, live_vert)

        # update the chapter
        self.old_mongo.update_children(chapter1.location, chapter1.children)

        # now the other one w/ the conditional
        # first create some show children
        indirect1 = self._create_and_get_item(
            self.old_mongo,
            location.replace(category='discussion', name=uuid.uuid4().hex),
            "", {'display_name': 'conditional show 1'}, runtime
        )
        indirect2 = self._create_and_get_item(
            self.old_mongo,
            location.replace(category='html', name=uuid.uuid4().hex),
            "", {'display_name': 'conditional show 2'}, runtime
        )
        location = location.replace(category='conditional', name=uuid.uuid4().hex)
        metadata = {
            'xml_attributes': {
                'sources': [live_vert.location.url(), ],
                'completed': True,
            },
        }
        data = {
            'show_tag_list': [indirect1.location.url(), indirect2.location.url()]
        }
        conditional = self._create_and_get_item(self.old_mongo, location, data, metadata, runtime)
        conditional.children = [indirect1.location.url(), indirect2.location.url()]
        # add direct children
        self.create_random_units(self.old_mongo, conditional)
        chapter2.children.append(conditional.location.url())
        self.old_mongo.update_children(chapter2.location, chapter2.children)

        # and the ancillary docs (not children)
        location = location.replace(category='static_tab', name=uuid.uuid4().hex)
        # the below automatically adds the tab to the course
        _tab = self._create_and_get_item(self.old_mongo, location, "", {'display_name': 'Tab uno'}, runtime)

        location = location.replace(category='about', name='overview')
        _overview = self._create_and_get_item(self.old_mongo, location, "<p>test</p>", {}, runtime)
        location = location.replace(category='course_info', name='updates')
        _overview = self._create_and_get_item(
            self.old_mongo,
            location, "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, runtime
        )

    def create_random_units(self, store, parent, cc_store=None, cc_parent=None):
        """
        Create a random selection of units under the given parent w/ random names & attrs
        :param store: which store (e.g., direct/draft) to create them in
        :param parent: the parent to have point to them
        :param cc_store: (optional) if given, make a small change and save also to this store but w/ same location
        (only makes sense if store is 'direct' and this is 'draft' or vice versa)
        """
        for _ in range(random.randrange(6)):
            location = parent.location.replace(
                category=random.choice(['html', 'video', 'problem', 'discussion']),
                name=uuid.uuid4().hex
            )
            metadata = {'display_name': str(uuid.uuid4()), 'graded': True}
            data = {}
            element = self._create_and_get_item(store, location, data, metadata, parent.runtime)
            parent.children.append(element.location.url())
            if cc_store is not None:
                # change display_name and remove graded to test the delta
                element = self._create_and_get_item(
                    cc_store, location, data, {'display_name': str(uuid.uuid4())}, parent.runtime
                )
                cc_parent.children.append(element.location.url())
        store.update_children(parent.location, parent.children)
        if cc_store is not None:
            cc_store.update_children(cc_parent.location, cc_parent.children)

    def compare_courses(self, presplit, published):
        # descend via children to do comparison
        old_root = presplit.get_item(self.course_location, depth=None)
        new_root_locator = self.loc_mapper.translate_location(
            self.course_location.course_id, self.course_location, published, add_entry_if_missing=False
        )
        new_root = self.split_mongo.get_course(new_root_locator)
        self.compare_dags(presplit, old_root, new_root, published)

        # grab the detached items to compare they should be in both published and draft
        for category in ['conditional', 'about', 'course_info', 'static_tab']:
            location = self.course_location.replace(name=None, category=category)
            for conditional in presplit.get_items(location):
                locator = self.loc_mapper.translate_location(
                    self.course_location.course_id,
                    conditional.location, published, add_entry_if_missing=False
                )
                self.compare_dags(presplit, conditional, self.split_mongo.get_item(locator), published)

    def compare_dags(self, presplit, presplit_dag_root, split_dag_root, published):
        # check that locations match
        self.assertEqual(
            presplit_dag_root.location,
            self.loc_mapper.translate_locator_to_location(split_dag_root.location).replace(revision=None)
        )
        # compare all fields but children
        for name in presplit_dag_root.fields.iterkeys():
            if name != 'children':
                self.assertEqual(
                    getattr(presplit_dag_root, name),
                    getattr(split_dag_root, name),
                    "{}/{}: {} != {}".format(
                        split_dag_root.location, name, getattr(presplit_dag_root, name), getattr(split_dag_root, name)
                    )
                )
        # test split get_item using old Location: old draft store didn't set revision for things above vertical
        # but split does distinguish these; so, set revision if not published
        if not published:
            location = draft.as_draft(presplit_dag_root.location)
        else:
            location = presplit_dag_root.location
        refetched = self.split_mongo.get_item(location)
        self.assertEqual(
            refetched.location, split_dag_root.location,
            "Fetch from split via old Location {} not same as new {}".format(
                refetched.location, split_dag_root.location
            )
        )
        # compare children
        if presplit_dag_root.has_children:
            self.assertEqual(
                len(presplit_dag_root.get_children()), len(split_dag_root.get_children()),
                "{0.category} '{0.display_name}': children count {1} != {2}".format(
                    presplit_dag_root, len(presplit_dag_root.get_children()), split_dag_root.children
                )
            )
            for pre_child, split_child in zip(presplit_dag_root.get_children(), split_dag_root.get_children()):
                self.compare_dags(presplit, pre_child, split_child, published)

    def test_migrator(self):
        self.migrator.migrate_mongo_course(self.course_location, random.getrandbits(32))
        # now compare the migrated to the original course
        self.compare_courses(self.old_mongo, True)
        self.compare_courses(self.draft_mongo, False)
コード例 #23
0
class SplitWMongoCourseBoostrapper(unittest.TestCase):
    """
    Helper for tests which need to construct split mongo & old mongo based courses to get interesting internal structure.
    Override _create_course and after invoking the super() _create_course, have it call _create_item for
    each xblock you want in the course.
    This class ensures the db gets created, opened, and cleaned up in addition to creating the course

    Defines the following attrs on self:
    * user_id: a random non-registered mock user id
    * split_mongo: a pointer to the split mongo instance
    * draft_mongo: a pointer to the old draft instance
    * split_course_key (CourseLocator): of the new course
    * old_course_key: the SlashSpecifiedCourseKey for the course
    """
        # Snippet of what would be in the django settings envs file
    db_config = {
        'host': MONGO_HOST,
        'port': MONGO_PORT_NUM,
        'db': 'test_xmodule',
    }

    modulestore_options = {
        'default_class': 'xmodule.raw_module.RawDescriptor',
        'fs_root': '',
        'render_template': mock.Mock(return_value=""),
        'xblock_mixins': (InheritanceMixin,)
    }

    split_course_key = CourseLocator('test_org', 'test_course', 'runid', branch=ModuleStoreEnum.BranchName.draft)

    def setUp(self):
        self.db_config['collection'] = 'modulestore{0}'.format(uuid.uuid4().hex[:5])

        self.user_id = random.getrandbits(32)
        super(SplitWMongoCourseBoostrapper, self).setUp()
        self.split_mongo = SplitMongoModuleStore(
            None,
            self.db_config,
            **self.modulestore_options
        )
        self.addCleanup(self.split_mongo.db.connection.close)
        self.addCleanup(self.tear_down_split)
        self.draft_mongo = DraftMongoModuleStore(
            None, self.db_config, branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred, **self.modulestore_options
        )
        self.addCleanup(self.tear_down_mongo)
        self.old_course_key = None
        self.runtime = None
        self._create_course()

    def tear_down_split(self):
        """
        Remove the test collections, close the db connection
        """
        split_db = self.split_mongo.db
        split_db.drop_collection(split_db.course_index)
        split_db.drop_collection(split_db.structures)
        split_db.drop_collection(split_db.definitions)

    def tear_down_mongo(self):
        """
        Remove the test collections, close the db connection
        """
        split_db = self.split_mongo.db
        # old_mongo doesn't give a db attr, but all of the dbs are the same
        split_db.drop_collection(self.draft_mongo.collection)

    def _create_item(self, category, name, data, metadata, parent_category, parent_name, draft=True, split=True):
        """
        Create the item of the given category and block id in split and old mongo, add it to the optional
        parent. The parent category is only needed because old mongo requires it for the id.

        Note: if draft = False, it will create the draft and then publish it; so, it will overwrite any
        existing draft for both the new item and the parent
        """
        location = self.old_course_key.make_usage_key(category, name)
        self.draft_mongo.create_item(
            self.user_id,
            location.course_key,
            location.block_type,
            block_id=location.block_id,
            definition_data=data,
            metadata=metadata,
            runtime=self.runtime
        )
        if not draft:
            self.draft_mongo.publish(location, self.user_id)
        if isinstance(data, basestring):
            fields = {'data': data}
        else:
            fields = data.copy()
        fields.update(metadata)
        if parent_name:
            # add child to parent in mongo
            parent_location = self.old_course_key.make_usage_key(parent_category, parent_name)
            parent = self.draft_mongo.get_item(parent_location)
            parent.children.append(location)
            self.draft_mongo.update_item(parent, self.user_id)
            if not draft:
                self.draft_mongo.publish(parent_location, self.user_id)
            # create child for split
            if split:
                self.split_mongo.create_child(
                    self.user_id,
                    BlockUsageLocator(
                        course_key=self.split_course_key,
                        block_type=parent_category,
                        block_id=parent_name
                    ),
                    category,
                    block_id=name,
                    fields=fields
                )
        else:
            if split:
                self.split_mongo.create_item(
                    self.user_id,
                    self.split_course_key,
                    category,
                    block_id=name,
                    fields=fields
                )

    def _create_course(self, split=True):
        """
        * some detached items
        * some attached children
        * some orphans
        """
        metadata = {
            'start': datetime.datetime(2000, 3, 13, 4),
            'display_name': 'Migration test course',
        }
        data = {
            'wiki_slug': 'test_course_slug'
        }
        fields = metadata.copy()
        fields.update(data)
        if split:
            # split requires the course to be created separately from creating items
            self.split_mongo.create_course(
                self.split_course_key.org, self.split_course_key.course, self.split_course_key.run, self.user_id, fields=fields, root_block_id='runid'
            )
        old_course = self.draft_mongo.create_course(self.split_course_key.org, 'test_course', 'runid', self.user_id, fields=fields)
        self.old_course_key = old_course.id
        self.runtime = old_course.runtime
コード例 #24
0
class TestMigration(unittest.TestCase):
    """
    Test the split migrator
    """

    # Snippet of what would be in the django settings envs file
    db_config = {
        'host': 'localhost',
        'db': 'test_xmodule',
        'collection': 'modulestore{0}'.format(uuid.uuid4().hex[:5]),
    }

    modulestore_options = {
        'default_class': 'xmodule.raw_module.RawDescriptor',
        'fs_root': '',
        'render_template': mock.Mock(return_value=""),
        'xblock_mixins': (InheritanceMixin, )
    }

    def setUp(self):
        super(TestMigration, self).setUp()
        # pylint: disable=W0142
        self.loc_mapper = LocMapperStore(test_location_mapper.TrivialCache(),
                                         **self.db_config)
        self.old_mongo = MongoModuleStore(self.db_config,
                                          **self.modulestore_options)
        self.draft_mongo = DraftModuleStore(self.db_config,
                                            **self.modulestore_options)
        self.split_mongo = SplitMongoModuleStore(
            doc_store_config=self.db_config,
            loc_mapper=self.loc_mapper,
            **self.modulestore_options)
        self.migrator = SplitMigrator(self.split_mongo, self.old_mongo,
                                      self.draft_mongo, self.loc_mapper)
        self.course_location = None
        self.create_source_course()

    def tearDown(self):
        dbref = self.loc_mapper.db
        dbref.drop_collection(self.loc_mapper.location_map)
        split_db = self.split_mongo.db
        split_db.drop_collection(self.split_mongo.db_connection.course_index)
        split_db.drop_collection(self.split_mongo.db_connection.structures)
        split_db.drop_collection(self.split_mongo.db_connection.definitions)
        # old_mongo doesn't give a db attr, but all of the dbs are the same
        dbref.drop_collection(self.old_mongo.collection)

        dbref.connection.close()

        super(TestMigration, self).tearDown()

    def _create_and_get_item(self,
                             store,
                             location,
                             data,
                             metadata,
                             runtime=None):
        store.create_and_save_xmodule(location, data, metadata, runtime)
        return store.get_item(location)

    def create_source_course(self):
        """
        A course testing all of the conversion mechanisms:
        * some inheritable settings
        * sequences w/ draft and live intermixed children to ensure all get to the draft but
        only the live ones get to published. Some are only draft, some are both, some are only live.
        * about, static_tab, and conditional documents
        """
        location = Location('i4x', 'test_org', 'test_course', 'course',
                            'runid')
        self.course_location = location
        date_proxy = Date()
        metadata = {
            'start': date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)),
            'display_name': 'Migration test course',
        }
        data = {'wiki_slug': 'test_course_slug'}
        course_root = self._create_and_get_item(self.old_mongo, location, data,
                                                metadata)
        runtime = course_root.runtime
        # chapters
        location = location.replace(category='chapter', name=uuid.uuid4().hex)
        chapter1 = self._create_and_get_item(self.old_mongo, location, {},
                                             {'display_name': 'Chapter 1'},
                                             runtime)
        course_root.children.append(chapter1.location.url())
        location = location.replace(category='chapter', name=uuid.uuid4().hex)
        chapter2 = self._create_and_get_item(self.old_mongo, location, {},
                                             {'display_name': 'Chapter 2'},
                                             runtime)
        course_root.children.append(chapter2.location.url())
        self.old_mongo.update_item(course_root, '**replace_user**')
        # vertical in live only
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        live_vert = self._create_and_get_item(
            self.old_mongo, location, {}, {'display_name': 'Live vertical'},
            runtime)
        chapter1.children.append(live_vert.location.url())
        self.create_random_units(self.old_mongo, live_vert)
        # vertical in both live and draft
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        both_vert = self._create_and_get_item(
            self.old_mongo, location, {}, {'display_name': 'Both vertical'},
            runtime)
        draft_both = self._create_and_get_item(
            self.draft_mongo, location, {},
            {'display_name': 'Both vertical renamed'}, runtime)
        chapter1.children.append(both_vert.location.url())
        self.create_random_units(self.old_mongo, both_vert, self.draft_mongo,
                                 draft_both)
        # vertical in draft only (x2)
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        draft_vert = self._create_and_get_item(
            self.draft_mongo, location, {}, {'display_name': 'Draft vertical'},
            runtime)
        chapter1.children.append(draft_vert.location.url())
        self.create_random_units(self.draft_mongo, draft_vert)
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        draft_vert = self._create_and_get_item(
            self.draft_mongo, location, {},
            {'display_name': 'Draft vertical2'}, runtime)
        chapter1.children.append(draft_vert.location.url())
        self.create_random_units(self.draft_mongo, draft_vert)
        # and finally one in live only (so published has to skip 2)
        location = location.replace(category='vertical', name=uuid.uuid4().hex)
        live_vert = self._create_and_get_item(
            self.old_mongo, location, {},
            {'display_name': 'Live vertical end'}, runtime)
        chapter1.children.append(live_vert.location.url())
        self.create_random_units(self.old_mongo, live_vert)

        # update the chapter
        self.old_mongo.update_item(chapter1, '**replace_user**')

        # now the other one w/ the conditional
        # first create some show children
        indirect1 = self._create_and_get_item(
            self.old_mongo,
            location.replace(category='discussion', name=uuid.uuid4().hex), "",
            {'display_name': 'conditional show 1'}, runtime)
        indirect2 = self._create_and_get_item(
            self.old_mongo,
            location.replace(category='html', name=uuid.uuid4().hex), "",
            {'display_name': 'conditional show 2'}, runtime)
        location = location.replace(category='conditional',
                                    name=uuid.uuid4().hex)
        metadata = {
            'xml_attributes': {
                'sources': [
                    live_vert.location.url(),
                ],
                'completed': True,
            },
        }
        data = {
            'show_tag_list':
            [indirect1.location.url(),
             indirect2.location.url()]
        }
        conditional = self._create_and_get_item(self.old_mongo, location, data,
                                                metadata, runtime)
        conditional.children = [
            indirect1.location.url(),
            indirect2.location.url()
        ]
        # add direct children
        self.create_random_units(self.old_mongo, conditional)
        chapter2.children.append(conditional.location.url())
        self.old_mongo.update_item(chapter2, '**replace_user**')

        # and the ancillary docs (not children)
        location = location.replace(category='static_tab',
                                    name=uuid.uuid4().hex)
        # the below automatically adds the tab to the course
        _tab = self._create_and_get_item(self.old_mongo, location, "",
                                         {'display_name': 'Tab uno'}, runtime)

        location = location.replace(category='about', name='overview')
        _overview = self._create_and_get_item(self.old_mongo, location,
                                              "<p>test</p>", {}, runtime)
        location = location.replace(category='course_info', name='updates')
        _overview = self._create_and_get_item(
            self.old_mongo, location,
            "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, runtime)

    def create_random_units(self,
                            store,
                            parent,
                            cc_store=None,
                            cc_parent=None):
        """
        Create a random selection of units under the given parent w/ random names & attrs
        :param store: which store (e.g., direct/draft) to create them in
        :param parent: the parent to have point to them
        :param cc_store: (optional) if given, make a small change and save also to this store but w/ same location
        (only makes sense if store is 'direct' and this is 'draft' or vice versa)
        """
        for _ in range(random.randrange(6)):
            location = parent.location.replace(category=random.choice(
                ['html', 'video', 'problem', 'discussion']),
                                               name=uuid.uuid4().hex)
            metadata = {'display_name': str(uuid.uuid4()), 'graded': True}
            data = {}
            element = self._create_and_get_item(store, location, data,
                                                metadata, parent.runtime)
            parent.children.append(element.location.url())
            if cc_store is not None:
                # change display_name and remove graded to test the delta
                element = self._create_and_get_item(
                    cc_store, location, data,
                    {'display_name': str(uuid.uuid4())}, parent.runtime)
                cc_parent.children.append(element.location.url())
        store.update_item(parent, '**replace_user**')
        if cc_store is not None:
            cc_store.update_item(cc_parent, '**replace_user**')

    def compare_courses(self, presplit, published):
        # descend via children to do comparison
        old_root = presplit.get_item(self.course_location, depth=None)
        new_root_locator = self.loc_mapper.translate_location(
            self.course_location.course_id,
            self.course_location,
            published,
            add_entry_if_missing=False)
        new_root = self.split_mongo.get_course(new_root_locator)
        self.compare_dags(presplit, old_root, new_root, published)

        # grab the detached items to compare they should be in both published and draft
        for category in ['conditional', 'about', 'course_info', 'static_tab']:
            location = self.course_location.replace(name=None,
                                                    category=category)
            for conditional in presplit.get_items(location):
                locator = self.loc_mapper.translate_location(
                    self.course_location.course_id,
                    conditional.location,
                    published,
                    add_entry_if_missing=False)
                self.compare_dags(presplit, conditional,
                                  self.split_mongo.get_item(locator),
                                  published)

    def compare_dags(self, presplit, presplit_dag_root, split_dag_root,
                     published):
        # check that locations match
        self.assertEqual(
            presplit_dag_root.location,
            self.loc_mapper.translate_locator_to_location(
                split_dag_root.location).replace(revision=None))
        # compare all fields but children
        for name in presplit_dag_root.fields.iterkeys():
            if name != 'children':
                self.assertEqual(
                    getattr(presplit_dag_root, name),
                    getattr(split_dag_root, name),
                    "{}/{}: {} != {}".format(split_dag_root.location, name,
                                             getattr(presplit_dag_root, name),
                                             getattr(split_dag_root, name)))
        # test split get_item using old Location: old draft store didn't set revision for things above vertical
        # but split does distinguish these; so, set revision if not published
        if not published:
            location = draft.as_draft(presplit_dag_root.location)
        else:
            location = presplit_dag_root.location
        refetched = self.split_mongo.get_item(location)
        self.assertEqual(
            refetched.location, split_dag_root.location,
            "Fetch from split via old Location {} not same as new {}".format(
                refetched.location, split_dag_root.location))
        # compare children
        if presplit_dag_root.has_children:
            self.assertEqual(
                len(presplit_dag_root.get_children()),
                len(split_dag_root.get_children()),
                "{0.category} '{0.display_name}': children count {1} != {2}".
                format(presplit_dag_root,
                       len(presplit_dag_root.get_children()),
                       split_dag_root.children))
            for pre_child, split_child in zip(presplit_dag_root.get_children(),
                                              split_dag_root.get_children()):
                self.compare_dags(presplit, pre_child, split_child, published)

    def test_migrator(self):
        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)