diff --git a/cms/djangoapps/contentstore/tests/test_crud.py b/cms/djangoapps/contentstore/tests/test_crud.py index 0e420d3ee8..8ecb40f60f 100644 --- a/cms/djangoapps/contentstore/tests/test_crud.py +++ b/cms/djangoapps/contentstore/tests/test_crud.py @@ -1,5 +1,4 @@ import unittest -from django.conf import settings from xmodule import templates from xmodule.modulestore.tests import persistent_factories @@ -10,8 +9,6 @@ from xmodule.capa_module import CapaDescriptor from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator, LocalId from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError from xmodule.html_module import HtmlDescriptor -from xmodule.modulestore import inheritance -from xblock.core import XBlock from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -57,14 +54,14 @@ class TemplateTests(unittest.TestCase): def test_factories(self): test_course = persistent_factories.PersistentCourseFactory.create( - course_id='testx.tempcourse', org='testx', prettyid='tempcourse', + course_id='testx.tempcourse', org='testx', display_name='fun test course', user_id='testbot' ) self.assertIsInstance(test_course, CourseDescriptor) self.assertEqual(test_course.display_name, 'fun test course') index_info = modulestore('split').get_course_index_info(test_course.location) self.assertEqual(index_info['org'], 'testx') - self.assertEqual(index_info['prettyid'], 'tempcourse') + self.assertEqual(index_info['_id'], 'testx.tempcourse') test_chapter = persistent_factories.ItemFactory.create(display_name='chapter 1', parent_location=test_course.location) @@ -75,31 +72,31 @@ class TemplateTests(unittest.TestCase): with self.assertRaises(DuplicateCourseError): persistent_factories.PersistentCourseFactory.create( - course_id='testx.tempcourse', org='testx', prettyid='tempcourse', + course_id='testx.tempcourse', org='testx', display_name='fun test course', user_id='testbot' ) def test_temporary_xblocks(self): """ - Test using load_from_json to create non persisted xblocks + Test create_xblock to create non persisted xblocks """ test_course = persistent_factories.PersistentCourseFactory.create( - course_id='testx.tempcourse', org='testx', prettyid='tempcourse', + course_id='testx.tempcourse', org='testx', display_name='fun test course', user_id='testbot' ) - test_chapter = self.load_from_json({'category': 'chapter', - 'fields': {'display_name': 'chapter n'}}, - test_course.system, parent_xblock=test_course) + test_chapter = modulestore('split').create_xblock( + test_course.system, 'chapter', {'display_name': 'chapter n'}, parent_xblock=test_course + ) self.assertIsInstance(test_chapter, SequenceDescriptor) self.assertEqual(test_chapter.display_name, 'chapter n') self.assertIn(test_chapter, test_course.get_children()) # test w/ a definition (e.g., a problem) test_def_content = 'boo' - test_problem = self.load_from_json({'category': 'problem', - 'fields': {'data': test_def_content}}, - test_course.system, parent_xblock=test_chapter) + test_problem = modulestore('split').create_xblock( + test_course.system, 'problem', {'data': test_def_content}, parent_xblock=test_chapter + ) self.assertIsInstance(test_problem, CapaDescriptor) self.assertEqual(test_problem.data, test_def_content) self.assertIn(test_problem, test_chapter.get_children()) @@ -111,20 +108,22 @@ class TemplateTests(unittest.TestCase): try saving temporary xblocks """ test_course = persistent_factories.PersistentCourseFactory.create( - course_id='testx.tempcourse', org='testx', prettyid='tempcourse', - display_name='fun test course', user_id='testbot') - test_chapter = self.load_from_json({'category': 'chapter', - 'fields': {'display_name': 'chapter n'}}, - test_course.system, parent_xblock=test_course) + course_id='testx.tempcourse', org='testx', + display_name='fun test course', user_id='testbot' + ) + test_chapter = modulestore('split').create_xblock( + test_course.system, 'chapter', {'display_name': 'chapter n'}, parent_xblock=test_course + ) + self.assertEqual(test_chapter.display_name, 'chapter n') test_def_content = 'boo' # create child - new_block = self.load_from_json({ - 'category': 'problem', - 'fields': { + new_block = modulestore('split').create_xblock( + test_course.system, + 'problem', + fields={ 'data': test_def_content, 'display_name': 'problem' - }}, - test_course.system, + }, parent_xblock=test_chapter ) self.assertIsNotNone(new_block.definition_locator) @@ -149,7 +148,6 @@ class TemplateTests(unittest.TestCase): def test_delete_course(self): test_course = persistent_factories.PersistentCourseFactory.create( course_id='edu.harvard.history.doomed', org='testx', - prettyid='edu.harvard.history.doomed', display_name='doomed test course', user_id='testbot') persistent_factories.ItemFactory.create(display_name='chapter 1', @@ -173,9 +171,9 @@ class TemplateTests(unittest.TestCase): """ test_course = persistent_factories.PersistentCourseFactory.create( course_id='edu.harvard.history.hist101', org='testx', - prettyid='edu.harvard.history.hist101', display_name='history test course', - user_id='testbot') + user_id='testbot' + ) chapter = persistent_factories.ItemFactory.create(display_name='chapter 1', parent_location=test_course.location, user_id='testbot') sub = persistent_factories.ItemFactory.create(display_name='subsection 1', @@ -242,44 +240,3 @@ class TemplateTests(unittest.TestCase): mapper = loc_mapper() self.assertEqual(modulestore('split').loc_mapper, mapper) - # ================================= JSON PARSING =========================== - # These are example methods for creating xmodules in memory w/o persisting them. - # They were in x_module but since xblock is not planning to support them but will - # allow apps to use this type of thing, I put it here. - @staticmethod - def load_from_json(json_data, system, default_class=None, parent_xblock=None): - """ - This method instantiates the correct subclass of XModuleDescriptor based - on the contents of json_data. It does not persist it and can create one which - has no usage id. - - parent_xblock is used to compute inherited metadata as well as to append the new xblock. - - json_data: - - 'location' : must have this field - - 'category': the xmodule category (required or location must be a Location) - - 'metadata': a dict of locally set metadata (not inherited) - - 'children': a list of children's usage_ids w/in this course - - 'definition': - - '_id' (optional): the usage_id of this. Will generate one if not given one. - """ - class_ = XBlock.load_class( - json_data.get('category', json_data.get('location', {}).get('category')), - default_class, - select=settings.XBLOCK_SELECT_FUNCTION - ) - usage_id = json_data.get('_id', None) - if not '_inherited_settings' in json_data and parent_xblock is not None: - json_data['_inherited_settings'] = parent_xblock.xblock_kvs.inherited_settings.copy() - json_fields = json_data.get('fields', {}) - for field_name in inheritance.InheritanceMixin.fields: - if field_name in json_fields: - json_data['_inherited_settings'][field_name] = json_fields[field_name] - - new_block = system.xblock_from_json(class_, usage_id, json_data) - if parent_xblock is not None: - parent_xblock.children.append(new_block.scope_ids.usage_id) - # decache pending children field settings - parent_xblock.save() - return new_block - diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index af7f1324de..6da30a1555 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -97,7 +97,7 @@ def course_handler(request, tag=None, package_id=None, branch=None, version_guid index entry. PUT json: update this course (index entry not xblock) such as repointing head, changing display name, org, - package_id, prettyid. Return same json as above. + package_id. Return same json as above. DELETE json: delete this branch from this course (leaving off /branch/draft would imply delete the course) """ diff --git a/common/lib/xmodule/xmodule/modulestore/locator.py b/common/lib/xmodule/xmodule/modulestore/locator.py index 4c384a49f1..a51ac765e1 100644 --- a/common/lib/xmodule/xmodule/modulestore/locator.py +++ b/common/lib/xmodule/xmodule/modulestore/locator.py @@ -22,12 +22,14 @@ log = logging.getLogger(__name__) class LocalId(object): """ - Class for local ids for non-persisted xblocks - - Should be hashable and distinguishable, but nothing else + Class for local ids for non-persisted xblocks (which can have hardcoded block_ids if necessary) """ + def __init__(self, block_id=None): + self.block_id = block_id + super(LocalId, self).__init__() + def __str__(self): - return "localid_{}".format(id(self)) + return "localid_{}".format(self.block_id or id(self)) class Locator(object): @@ -358,8 +360,7 @@ class CourseLocator(Locator): Generate a discussion group id based on course To make compatible with old Location object functionality. I don't believe this behavior fits at this - place, but I have no way to override. If this is really needed, it should probably use the pretty_id to seed - the name although that's mutable. We should also clearly define the purpose and restrictions of this + place, but I have no way to override. We should clearly define the purpose and restrictions of this (e.g., I'm assuming periods are fine). """ return self.package_id diff --git a/common/lib/xmodule/xmodule/modulestore/mixed.py b/common/lib/xmodule/xmodule/modulestore/mixed.py index ccde9b3c9c..1304a902b4 100644 --- a/common/lib/xmodule/xmodule/modulestore/mixed.py +++ b/common/lib/xmodule/xmodule/modulestore/mixed.py @@ -282,7 +282,6 @@ class MixedModuleStore(ModuleStoreWriteBase): :param fields: a dict of xblock field name - value pairs for the course module. :param metadata: the old way of setting fields by knowing which ones are scope.settings v scope.content :param definition_data: the complement to metadata which is also a subset of fields - :param pretty_id: a field split.create_course uses and may quit using :returns: course xblock """ store = self.modulestores[store_name] @@ -297,11 +296,10 @@ class MixedModuleStore(ModuleStoreWriteBase): org = None org = kwargs.pop('org', org) - pretty_id = kwargs.pop('pretty_id', course_id) fields = kwargs.pop('fields', {}) fields.update(kwargs.pop('metadata', {})) fields.update(kwargs.pop('definition_data', {})) - course = store.create_course(course_id, org, pretty_id, user_id, fields=fields, **kwargs) + course = store.create_course(course_id, org, user_id, fields=fields, **kwargs) else: # assume mongo course = store.create_course(course_id, **kwargs) diff --git a/common/lib/xmodule/xmodule/modulestore/split_migrator.py b/common/lib/xmodule/xmodule/modulestore/split_migrator.py index 516e37607a..80eba98029 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_migrator.py +++ b/common/lib/xmodule/xmodule/modulestore/split_migrator.py @@ -47,7 +47,7 @@ class SplitMigrator(object): original_course = self.direct_modulestore.get_item(course_location) new_course_root_locator = self.loc_mapper.translate_location(old_course_id, course_location) new_course = self.split_modulestore.create_course( - new_package_id, course_location.org, original_course.display_name, + new_package_id, course_location.org, user.id, fields=self._get_json_fields_translate_children(original_course, old_course_id, True), root_block_id=new_course_root_locator.block_id, diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 360028cead..10e9185dee 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -5,7 +5,6 @@ Representation: * course_index: a dictionary: ** '_id': package_id (e.g., myu.mydept.mycourse.myrun), ** 'org': the org's id. Only used for searching not identity, - ** 'prettyid': a vague to-be-determined field probably more useful to storing searchable tags, ** 'edited_by': user_id of user who created the original entry, ** 'edited_on': the datetime of the original creation, ** 'versions': versions_dict: {branch_id: structure_id, ...} @@ -99,6 +98,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): A Mongodb backed ModuleStore supporting versions, inheritance, and sharing. """ + + SCHEMA_VERSION = 1 reference_type = Locator def __init__(self, doc_store_config, fs_root, render_template, default_class=None, @@ -468,7 +469,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): heads. This function is primarily for test verification but may serve some more general purpose. :param course_locator: must have a package_id set - :return {'org': , 'prettyid': , + :return {'org': string, versions: {'draft': the head draft version id, 'published': the head published version id if any, }, @@ -618,7 +619,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): "edited_on": datetime.datetime.now(UTC), "previous_version": None, "original_version": new_id, - } + }, + 'schema_version': self.SCHEMA_VERSION, } self.db_connection.insert_definition(document) definition_locator = DefinitionLocator(new_id) @@ -654,6 +656,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): old_definition['edit_info']['edited_on'] = datetime.datetime.now(UTC) # previous version id old_definition['edit_info']['previous_version'] = definition_locator.definition_id + old_definition['schema_version'] = self.SCHEMA_VERSION self.db_connection.insert_definition(old_definition) return DefinitionLocator(old_definition['_id']), True else: @@ -811,7 +814,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): return self.get_item(item_loc) def create_course( - self, course_id, org, prettyid, user_id, fields=None, + self, course_id, org, user_id, fields=None, master_branch='draft', versions_dict=None, root_category='course', root_block_id='course' ): @@ -870,7 +873,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): 'edited_on': datetime.datetime.now(UTC), 'previous_version': None, 'original_version': definition_id, - } + }, + 'schema_version': self.SCHEMA_VERSION, } self.db_connection.insert_definition(definition_entry) @@ -904,6 +908,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): definition['edit_info']['edited_by'] = user_id definition['edit_info']['edited_on'] = datetime.datetime.now(UTC) definition['_id'] = ObjectId() + definition['schema_version'] = self.SCHEMA_VERSION self.db_connection.insert_definition(definition) root_block['definition'] = definition['_id'] root_block['edit_info']['edited_on'] = datetime.datetime.now(UTC) @@ -917,10 +922,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): index_entry = { '_id': course_id, 'org': org, - 'prettyid': prettyid, 'edited_by': user_id, 'edited_on': datetime.datetime.now(UTC), - 'versions': versions_dict} + 'versions': versions_dict, + 'schema_version': self.SCHEMA_VERSION, + } self.db_connection.insert_course_index(index_entry) return self.get_course(CourseLocator(package_id=course_id, branch=master_branch)) @@ -947,7 +953,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): # check children original_entry = self._get_block_from_structure(original_structure, descriptor.location.block_id) is_updated = is_updated or ( - descriptor.has_children and original_entry['fields']['children'] != descriptor.children + descriptor.has_children and original_entry['fields'].get('children', []) != descriptor.children ) # check metadata if not is_updated: @@ -986,6 +992,40 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): # nothing changed, just return the one sent in return descriptor + def create_xblock(self, runtime, category, fields=None, block_id=None, definition_id=None, parent_xblock=None): + """ + This method instantiates the correct subclass of XModuleDescriptor based + on the contents of json_data. It does not persist it and can create one which + has no usage id. + + parent_xblock is used to compute inherited metadata as well as to append the new xblock. + + json_data: + - 'category': the xmodule category + - 'fields': a dict of locally set fields (not inherited) in json format not pythonic typed format! + - 'definition': the object id of the existing definition + """ + xblock_class = runtime.load_block_type(category) + json_data = { + 'category': category, + 'fields': fields or {}, + } + if definition_id is not None: + json_data['definition'] = definition_id + if parent_xblock is not None: + json_data['_inherited_settings'] = parent_xblock.xblock_kvs.inherited_settings.copy() + if fields is not None: + for field_name in inheritance.InheritanceMixin.fields: + if field_name in fields: + json_data['_inherited_settings'][field_name] = fields[field_name] + + new_block = runtime.xblock_from_json(xblock_class, block_id, json_data) + if parent_xblock is not None: + parent_xblock.children.append(new_block.scope_ids.usage_id) + # decache pending children field settings + parent_xblock.save() + return new_block + def persist_xblock_dag(self, xblock, user_id, force=False): """ create or update the xblock and all of its children. The xblock's location must specify a course. @@ -1044,8 +1084,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): # generate an id is_new = True is_updated = True - block_id = self._generate_block_id(structure_blocks, xblock.category) - encoded_block_id = block_id + block_id = getattr(xblock.scope_ids.usage_id.block_id, 'block_id', None) + if block_id is None: + block_id = self._generate_block_id(structure_blocks, xblock.category) + encoded_block_id = LocMapperStore.encode_key_for_mongo(block_id) xblock.scope_ids.usage_id.block_id = block_id else: is_new = False @@ -1448,6 +1490,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): new_structure['previous_version'] = structure['_id'] new_structure['edited_by'] = user_id new_structure['edited_on'] = datetime.datetime.now(UTC) + new_structure['schema_version'] = self.SCHEMA_VERSION return new_structure def _find_local_root(self, element_to_find, possibility, tree): @@ -1508,7 +1551,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): 'original_version': new_id, 'edited_by': user_id, 'edited_on': datetime.datetime.now(UTC), - 'blocks': blocks + 'blocks': blocks, + 'schema_version': self.SCHEMA_VERSION, } def _get_parents_from_structure(self, block_id, structure): diff --git a/common/lib/xmodule/xmodule/modulestore/tests/persistent_factories.py b/common/lib/xmodule/xmodule/modulestore/tests/persistent_factories.py index 34e3a89b22..9f6675f033 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/persistent_factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/persistent_factories.py @@ -24,7 +24,6 @@ class PersistentCourseFactory(SplitFactory): keywords: any xblock field plus (note, the below are filtered out; so, if they become legitimate xblock fields, they won't be settable via this factory) * org: defaults to textX - * prettyid: defaults to 999 * master_branch: (optional) defaults to 'draft' * user_id: (optional) defaults to 'test_user' * display_name (xblock field): will default to 'Robot Super Course' unless provided @@ -33,14 +32,14 @@ class PersistentCourseFactory(SplitFactory): # pylint: disable=W0613 @classmethod - def _create(cls, target_class, course_id='testX.999', org='testX', prettyid='999', user_id='test_user', + def _create(cls, target_class, course_id='testX.999', org='testX', user_id='test_user', master_branch='draft', **kwargs): modulestore = kwargs.pop('modulestore') root_block_id = kwargs.pop('root_block_id', 'course') # Write the data to the mongo datastore new_course = modulestore.create_course( - course_id, org, prettyid, user_id, fields=kwargs, + course_id, org, user_id, fields=kwargs, master_branch=master_branch, root_block_id=root_block_id ) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_orphan.py b/common/lib/xmodule/xmodule/modulestore/tests/test_orphan.py index 525bbab496..6202d9779d 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_orphan.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_orphan.py @@ -114,7 +114,7 @@ class TestOrphan(unittest.TestCase): fields.update(data) # split requires the course to be created separately from creating items self.split_mongo.create_course( - self.split_package_id, 'test_org', 'my course', self.userid, fields=fields, root_block_id='runid' + self.split_package_id, 'test_org', self.userid, fields=fields, root_block_id='runid' ) self.course_location = Location('i4x', 'test_org', 'test_course', 'course', 'runid') self.old_mongo.create_and_save_xmodule(self.course_location, data, metadata) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py index d52886b893..6ca20190bc 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py @@ -1,26 +1,25 @@ -''' -Created on Mar 25, 2013 - -@author: dmitchell -''' +""" + Test split modulestore w/o using any django stuff. +""" import datetime -import subprocess import unittest import uuid from importlib import import_module - -from xblock.fields import Scope -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemNotFoundError, VersionConflictError, \ - DuplicateItemError, DuplicateCourseError -from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator, VersionTree, DefinitionLocator -from xmodule.modulestore.inheritance import InheritanceMixin -from xmodule.x_module import XModuleMixin -from pytz import UTC from path import path import re import random +from xblock.fields import Scope +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.exceptions import (InsufficientSpecificationError, ItemNotFoundError, VersionConflictError, + DuplicateItemError, DuplicateCourseError) +from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator, VersionTree, LocalId +from xmodule.modulestore.inheritance import InheritanceMixin +from xmodule.x_module import XModuleMixin +from xmodule.fields import Date, Timedelta +from bson.objectid import ObjectId +from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore + class SplitModuleTest(unittest.TestCase): ''' @@ -52,52 +51,442 @@ class SplitModuleTest(unittest.TestCase): modulestore = None - # These version_guids correspond to values hard-coded in fixture files - # used for these tests. The files live in mitx/fixtures/splitmongo_json/* - - GUID_D0 = "1d00000000000000dddd0000" # v12345d - GUID_D1 = "1d00000000000000dddd1111" # v12345d1 - GUID_D2 = "1d00000000000000dddd2222" # v23456d - GUID_D3 = "1d00000000000000dddd3333" # v12345d0 - GUID_D4 = "1d00000000000000dddd4444" # v23456d0 - GUID_D5 = "1d00000000000000dddd5555" # v345679d - GUID_P = "1d00000000000000eeee0000" # v23456p - + _date_field = Date() + _time_delta_field = Timedelta() + COURSE_CONTENT = { + "testx.GreekHero": { + "org": "testx", + "root_block_id": "head12345", + "user_id": "test@edx.org", + "fields": { + "tabs": [ + { + "type": "courseware" + }, + { + "type": "course_info", + "name": "Course Info" + }, + { + "type": "discussion", + "name": "Discussion" + }, + { + "type": "wiki", + "name": "Wiki" + } + ], + "start": _date_field.from_json("2013-02-14T05:00"), + "display_name": "The Ancient Greek Hero", + "grading_policy": { + "GRADER": [ + { + "min_count": 5, + "weight": 0.15, + "type": "Homework", + "drop_count": 1, + "short_label": "HWa" + }, + { + "short_label": "", + "min_count": 2, + "type": "Lab", + "drop_count": 0, + "weight": 0.15 + }, + { + "short_label": "Midterm", + "min_count": 1, + "type": "Midterm Exam", + "drop_count": 0, + "weight": 0.3 + }, + { + "short_label": "Final", + "min_count": 1, + "type": "Final Exam", + "drop_count": 0, + "weight": 0.4 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.75 + }, + }, + }, + "revisions": [{ + "user_id": "testassist@edx.org", + "update": { + "head12345": { + "end": _date_field.from_json("2013-04-13T04:30"), + "tabs": [ + { + "type": "courseware" + }, + { + "type": "course_info", + "name": "Course Info" + }, + { + "type": "discussion", + "name": "Discussion" + }, + { + "type": "wiki", + "name": "Wiki" + }, + { + "type": "static_tab", + "name": "Syllabus", + "url_slug": "01356a17b5924b17a04b7fc2426a3798" + }, + { + "type": "static_tab", + "name": "Advice for Students", + "url_slug": "57e9991c0d794ff58f7defae3e042e39" + } + ], + "graceperiod": _time_delta_field.from_json("2 hours 0 minutes 0 seconds"), + "grading_policy": { + "GRADER": [ + { + "min_count": 5, + "weight": 0.15, + "type": "Homework", + "drop_count": 1, + "short_label": "HWa" + }, + { + "short_label": "", + "min_count": 12, + "type": "Lab", + "drop_count": 2, + "weight": 0.15 + }, + { + "short_label": "Midterm", + "min_count": 1, + "type": "Midterm Exam", + "drop_count": 0, + "weight": 0.3 + }, + { + "short_label": "Final", + "min_count": 1, + "type": "Final Exam", + "drop_count": 0, + "weight": 0.4 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.55 + } + }, + }} + }, + {"user_id": "testassist@edx.org", + "update": + {"head12345": { + "end": _date_field.from_json("2013-06-13T04:30"), + "grading_policy": { + "GRADER": [ + { + "min_count": 4, + "weight": 0.15, + "type": "Homework", + "drop_count": 2, + "short_label": "HWa" + }, + { + "short_label": "", + "min_count": 12, + "type": "Lab", + "drop_count": 2, + "weight": 0.15 + }, + { + "short_label": "Midterm", + "min_count": 1, + "type": "Midterm Exam", + "drop_count": 0, + "weight": 0.3 + }, + { + "short_label": "Final", + "min_count": 1, + "type": "Final Exam", + "drop_count": 0, + "weight": 0.4 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.45 + } + }, + "enrollment_start": _date_field.from_json("2013-01-01T05:00"), + "enrollment_end": _date_field.from_json("2013-03-02T05:00"), + "advertised_start": "Fall 2013", + }}, + "create": [ + { + "id": "chapter1", + "parent": "head12345", + "category": "chapter", + "fields": { + "display_name": "Hercules" + }, + }, + { + "id": "chapter2", + "parent": "head12345", + "category": "chapter", + "fields": { + "display_name": "Hera heckles Hercules" + }, + }, + { + "id": "chapter3", + "parent": "head12345", + "category": "chapter", + "fields": { + "display_name": "Hera cuckolds Zeus" + }, + }, + { + "id": "problem1", + "parent": "chapter3", + "category": "problem", + "fields": { + "display_name": "Problem 3.1", + "graceperiod": "4 hours 0 minutes 0 seconds" + }, + }, + { + "id": "problem3_2", + "parent": "chapter3", + "category": "problem", + "fields": { + "display_name": "Problem 3.2" + }, + } + ] + }, + ] + }, + "testx.wonderful": { + "org": "testx", + "root_block_id": "head23456", + "user_id": "test@edx.org", + "fields": { + "tabs": [ + { + "type": "courseware" + }, + { + "type": "course_info", + "name": "Course Info" + }, + { + "type": "discussion", + "name": "Discussion" + }, + { + "type": "wiki", + "name": "Wiki" + } + ], + "start": _date_field.from_json("2013-02-14T05:00"), + "display_name": "A wonderful course", + "grading_policy": { + "GRADER": [ + { + "min_count": 14, + "weight": 0.25, + "type": "Homework", + "drop_count": 1, + "short_label": "HWa" + }, + { + "short_label": "", + "min_count": 12, + "type": "Lab", + "drop_count": 2, + "weight": 0.25 + }, + { + "short_label": "Midterm", + "min_count": 1, + "type": "Midterm Exam", + "drop_count": 0, + "weight": 0.2 + }, + { + "short_label": "Final", + "min_count": 1, + "type": "Final Exam", + "drop_count": 0, + "weight": 0.3 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.95 + } + }, + }, + "revisions": [{ + "user_id": "test@edx.org", + "update": { + "head23456": { + "display_name": "The most wonderful course", + "grading_policy": { + "GRADER": [ + { + "min_count": 14, + "weight": 0.25, + "type": "Homework", + "drop_count": 1, + "short_label": "HWa" + }, + { + "short_label": "", + "min_count": 12, + "type": "Lab", + "drop_count": 2, + "weight": 0.25 + }, + { + "short_label": "Midterm", + "min_count": 1, + "type": "Midterm Exam", + "drop_count": 0, + "weight": 0.2 + }, + { + "short_label": "Final", + "min_count": 1, + "type": "Final Exam", + "drop_count": 0, + "weight": 0.3 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.45 + } + }, + } + } + } + ] + }, + "guestx.contender": { + "org": "guestx", + "root_block_id": "head345679", + "user_id": "test@guestx.edu", + "fields": { + "tabs": [ + { + "type": "courseware" + }, + { + "type": "course_info", + "name": "Course Info" + }, + { + "type": "discussion", + "name": "Discussion" + }, + { + "type": "wiki", + "name": "Wiki" + } + ], + "start": _date_field.from_json("2013-03-14T05:00"), + "display_name": "Yet another contender", + "grading_policy": { + "GRADER": [ + { + "min_count": 4, + "weight": 0.25, + "type": "Homework", + "drop_count": 0, + "short_label": "HW" + }, + { + "short_label": "Midterm", + "min_count": 1, + "type": "Midterm Exam", + "drop_count": 0, + "weight": 0.4 + }, + { + "short_label": "Final", + "min_count": 1, + "type": "Final Exam", + "drop_count": 0, + "weight": 0.35 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.25 + } + }, + } + }, + } @staticmethod def bootstrapDB(): ''' - Loads the initial data into the db ensuring the collection name is - unique. + Sets up the initial data into the db ''' - collection_prefix = SplitModuleTest.MODULESTORE['DOC_STORE_CONFIG']['collection'] + '.' - dbname = SplitModuleTest.MODULESTORE['DOC_STORE_CONFIG']['db'] - processes = [ - subprocess.Popen([ - 'mongoimport', '-d', dbname, '-c', - collection_prefix + collection, '--jsonArray', - '--file', - SplitModuleTest.COMMON_ROOT + '/test/data/splitmongo_json/' + collection + '.json' - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + split_store = modulestore() + for course_id, course_spec in SplitModuleTest.COURSE_CONTENT.iteritems(): + course = split_store.create_course( + course_id, course_spec['org'], course_spec['user_id'], + fields=course_spec['fields'], + root_block_id=course_spec['root_block_id'] ) - for collection in ('active_versions', 'structures', 'definitions')] - for p in processes: - stdout, stderr = p.communicate() - if p.returncode != 0: - print "Couldn't run mongoimport:" - print stdout - print stderr - raise Exception("DB did not init correctly") + for revision in course_spec.get('revisions', []): + for block_id, fields in revision.get('update', {}).iteritems(): + # cheat since course is most frequent + if course.location.block_id == block_id: + block = course + else: + block_usage = BlockUsageLocator.make_relative(course.location, block_id) + block = split_store.get_instance(course.location.package_id, block_usage) + for key, value in fields.iteritems(): + setattr(block, key, value) + # create new blocks into dag: parent must already exist; thus, order is important + new_ele_dict = {} + for spec in revision.get('create', []): + if spec['parent'] in new_ele_dict: + parent = new_ele_dict.get(spec['parent']) + elif spec['parent'] == course.location.block_id: + parent = course + else: + block_usage = BlockUsageLocator.make_relative(course.location, spec['parent']) + parent = split_store.get_instance(course.location.package_id, block_usage) + block_id = LocalId(spec['id']) + child = split_store.create_xblock( + course.runtime, spec['category'], spec['fields'], block_id, parent_xblock=parent + ) + new_ele_dict[spec['id']] = child + course = split_store.persist_xblock_dag(course, revision['user_id']) + # publish "testx.wonderful" + to_publish = BlockUsageLocator(package_id="testx.wonderful", branch="draft", block_id="head23456") + destination = CourseLocator(package_id="testx.wonderful", branch="published") + split_store.xblock_publish("test@edx.org", to_publish, destination, [to_publish.block_id], None) - @classmethod - def tearDownClass(cls): + def tearDown(self): + """ + Clear persistence between each test. + """ collection_prefix = SplitModuleTest.MODULESTORE['DOC_STORE_CONFIG']['collection'] + '.' if SplitModuleTest.modulestore: for collection in ('active_versions', 'structures', 'definitions'): modulestore().db.drop_collection(collection_prefix + collection) # drop the modulestore to force re init SplitModuleTest.modulestore = None + super(SplitModuleTest, self).tearDown() def findByIdInResult(self, collection, _id): """ @@ -120,11 +509,7 @@ class SplitModuleCourseTests(SplitModuleTest): self.assertEqual(len(courses), 3, "Wrong number of courses") # check metadata -- NOTE no promised order course = self.findByIdInResult(courses, "head12345") - self.assertEqual(course.location.package_id, "GreekHero") - self.assertEqual( - str(course.location.version_guid), self.GUID_D0, - "course version mismatch" - ) + self.assertEqual(course.location.package_id, "testx.GreekHero") self.assertEqual(course.category, 'course', 'wrong category') self.assertEqual(len(course.tabs), 6, "wrong number of tabs") self.assertEqual( @@ -135,13 +520,9 @@ class SplitModuleCourseTests(SplitModuleTest): course.advertised_start, "Fall 2013", "advertised_start" ) - self.assertEqual( - len(course.children), 3, - "children") - self.assertEqual(str(course.definition_locator.definition_id), "ad00000000000000dddd0000") + self.assertEqual(len(course.children), 3, "children") # check dates and graders--forces loading of descriptor self.assertEqual(course.edited_by, "testassist@edx.org") - self.assertEqual(str(course.previous_version), self.GUID_D1) self.assertDictEqual(course.grade_cutoffs, {"Pass": 0.45}) def test_branch_requests(self): @@ -151,9 +532,7 @@ class SplitModuleCourseTests(SplitModuleTest): self.assertEqual(len(courses_published), 1, len(courses_published)) course = self.findByIdInResult(courses_published, "head23456") self.assertIsNotNone(course, "published courses") - self.assertEqual(course.location.package_id, "wonderful") - self.assertEqual(str(course.location.version_guid), self.GUID_P, - course.location.version_guid) + self.assertEqual(course.location.package_id, "testx.wonderful") self.assertEqual(course.category, 'course', 'wrong category') self.assertEqual(len(course.tabs), 4, "wrong number of tabs") self.assertEqual(course.display_name, "The most wonderful course", @@ -171,40 +550,31 @@ class SplitModuleCourseTests(SplitModuleTest): self.assertIsNotNone(self.findByIdInResult(courses, "head12345")) self.assertIsNotNone(self.findByIdInResult(courses, "head23456")) - courses = modulestore().get_courses( - branch='draft', - qualifiers={'edited_on': {"$lt": datetime.datetime(2013, 3, 28, 15)}}) - self.assertEqual(len(courses), 2) - - courses = modulestore().get_courses( - branch='draft', - qualifiers={'org': 'testx', "prettyid": "test_course"}) - self.assertEqual(len(courses), 1) - self.assertIsNotNone(self.findByIdInResult(courses, "head12345")) - def test_get_course(self): ''' Test the various calling forms for get_course ''' - locator = CourseLocator(version_guid=self.GUID_D1) + locator = CourseLocator(package_id="testx.GreekHero", branch="draft") + head_course = modulestore().get_course(locator) + self.assertNotEqual(head_course.location.version_guid, head_course.previous_version) + locator = CourseLocator(version_guid=head_course.previous_version) course = modulestore().get_course(locator) self.assertIsNone(course.location.package_id) - self.assertEqual(str(course.location.version_guid), self.GUID_D1) + self.assertEqual(course.location.version_guid, head_course.previous_version) self.assertEqual(course.category, 'course') self.assertEqual(len(course.tabs), 6) self.assertEqual(course.display_name, "The Ancient Greek Hero") self.assertEqual(course.graceperiod, datetime.timedelta(hours=2)) self.assertIsNone(course.advertised_start) self.assertEqual(len(course.children), 0) - self.assertEqual(str(course.definition_locator.definition_id), "ad00000000000000dddd0001") + self.assertNotEqual(course.definition_locator.definition_id, head_course.definition_locator.definition_id) # check dates and graders--forces loading of descriptor self.assertEqual(course.edited_by, "testassist@edx.org") self.assertDictEqual(course.grade_cutoffs, {"Pass": 0.55}) - locator = CourseLocator(package_id='GreekHero', branch='draft') + locator = CourseLocator(package_id='testx.GreekHero', branch='draft') course = modulestore().get_course(locator) - self.assertEqual(course.location.package_id, "GreekHero") - self.assertEqual(str(course.location.version_guid), self.GUID_D0) + self.assertEqual(course.location.package_id, "testx.GreekHero") self.assertEqual(course.category, 'course') self.assertEqual(len(course.tabs), 6) self.assertEqual(course.display_name, "The Ancient Greek Hero") @@ -214,14 +584,13 @@ class SplitModuleCourseTests(SplitModuleTest): self.assertEqual(course.edited_by, "testassist@edx.org") self.assertDictEqual(course.grade_cutoffs, {"Pass": 0.45}) - locator = CourseLocator(package_id='wonderful', branch='published') + locator = CourseLocator(package_id='testx.wonderful', branch='published') course = modulestore().get_course(locator) - self.assertEqual(course.location.package_id, "wonderful") - self.assertEqual(str(course.location.version_guid), self.GUID_P) + published_version = course.location.version_guid - locator = CourseLocator(package_id='wonderful', branch='draft') + locator = CourseLocator(package_id='testx.wonderful', branch='draft') course = modulestore().get_course(locator) - self.assertEqual(str(course.location.version_guid), self.GUID_D2) + self.assertNotEqual(course.location.version_guid, published_version) def test_get_course_negative(self): # Now negative testing @@ -231,13 +600,13 @@ class SplitModuleCourseTests(SplitModuleTest): modulestore().get_course, CourseLocator(package_id='nosuchthing', branch='draft')) self.assertRaises(ItemNotFoundError, modulestore().get_course, - CourseLocator(package_id='GreekHero', branch='published')) + CourseLocator(package_id='testx.GreekHero', branch='published')) def test_cache(self): """ Test that the mechanics of caching work. """ - locator = CourseLocator(version_guid=self.GUID_D0) + locator = CourseLocator(package_id='testx.GreekHero', branch='draft') course = modulestore().get_course(locator) block_map = modulestore().cache_items(course.system, course.children, depth=3) self.assertIn('chapter1', block_map) @@ -247,22 +616,32 @@ class SplitModuleCourseTests(SplitModuleTest): """ get_course_successors(course_locator, version_history_depth=1) """ - locator = CourseLocator(version_guid=self.GUID_D3) + locator = CourseLocator(package_id='testx.GreekHero', branch='draft') + course = modulestore().get_course(locator) + versions = [course.location.version_guid, course.previous_version] + locator = CourseLocator(version_guid=course.previous_version) + course = modulestore().get_course(locator) + versions.append(course.previous_version) + + locator = CourseLocator(version_guid=course.previous_version) result = modulestore().get_course_successors(locator) self.assertIsInstance(result, VersionTree) self.assertIsNone(result.locator.package_id) - self.assertEqual(str(result.locator.version_guid), self.GUID_D3) + self.assertEqual(result.locator.version_guid, versions[-1]) self.assertEqual(len(result.children), 1) - self.assertEqual(str(result.children[0].locator.version_guid), self.GUID_D1) + self.assertEqual(result.children[0].locator.version_guid, versions[-2]) self.assertEqual(len(result.children[0].children), 0, "descended more than one level") + result = modulestore().get_course_successors(locator, version_history_depth=2) self.assertEqual(len(result.children), 1) - self.assertEqual(str(result.children[0].locator.version_guid), self.GUID_D1) + self.assertEqual(result.children[0].locator.version_guid, versions[-2]) self.assertEqual(len(result.children[0].children), 1) + result = modulestore().get_course_successors(locator, version_history_depth=99) self.assertEqual(len(result.children), 1) - self.assertEqual(str(result.children[0].locator.version_guid), self.GUID_D1) + self.assertEqual(result.children[0].locator.version_guid, versions[-2]) self.assertEqual(len(result.children[0].children), 1) + self.assertEqual(result.children[0].children[0].locator.version_guid, versions[0]) class SplitModuleItemTests(SplitModuleTest): ''' @@ -273,97 +652,98 @@ class SplitModuleItemTests(SplitModuleTest): ''' has_item(BlockUsageLocator) ''' - package_id = 'GreekHero' + package_id = 'testx.GreekHero' + locator = CourseLocator(package_id=package_id, branch='draft') + course = modulestore().get_course(locator) + previous_version = course.previous_version # positive tests of various forms - locator = BlockUsageLocator(version_guid=self.GUID_D1, block_id='head12345') - self.assertTrue(modulestore().has_item(package_id, locator), - "couldn't find in %s" % self.GUID_D1) + locator = BlockUsageLocator(version_guid=previous_version, block_id='head12345') + self.assertTrue( + modulestore().has_item(package_id, locator), "couldn't find in %s" % previous_version + ) - locator = BlockUsageLocator(package_id='GreekHero', block_id='head12345', branch='draft') + locator = BlockUsageLocator(package_id='testx.GreekHero', block_id='head12345', branch='draft') self.assertTrue( modulestore().has_item(locator.package_id, locator), - "couldn't find in 12345" - ) - self.assertTrue( - modulestore().has_item(locator.package_id, BlockUsageLocator( - package_id=locator.package_id, - branch='draft', - block_id=locator.block_id - )), - "couldn't find in draft 12345" ) self.assertFalse( modulestore().has_item(locator.package_id, BlockUsageLocator( package_id=locator.package_id, branch='published', block_id=locator.block_id)), - "found in published 12345" - ) - locator.branch = 'draft' - self.assertTrue( - modulestore().has_item(locator.package_id, locator), - "not found in draft 12345" + "found in published head" ) # not a course obj - locator = BlockUsageLocator(package_id='GreekHero', block_id='chapter1', branch='draft') + locator = BlockUsageLocator(package_id='testx.GreekHero', block_id='chapter1', branch='draft') self.assertTrue( modulestore().has_item(locator.package_id, locator), "couldn't find chapter1" ) # in published course - locator = BlockUsageLocator(package_id="wonderful", block_id="head23456", branch='draft') + locator = BlockUsageLocator(package_id="testx.wonderful", block_id="head23456", branch='draft') self.assertTrue( modulestore().has_item( locator.package_id, BlockUsageLocator(package_id=locator.package_id, block_id=locator.block_id, branch='published') - ), "couldn't find in 23456" + ) ) locator.branch = 'published' - self.assertTrue(modulestore().has_item(package_id, locator), "couldn't find in 23456") + self.assertTrue(modulestore().has_item(package_id, locator), "couldn't find in published") def test_negative_has_item(self): # negative tests--not found # no such course or block - package_id = 'GreekHero' + package_id = 'testx.GreekHero' locator = BlockUsageLocator(package_id="doesnotexist", block_id="head23456", branch='draft') self.assertFalse(modulestore().has_item(package_id, locator)) - locator = BlockUsageLocator(package_id="wonderful", block_id="doesnotexist", branch='draft') + locator = BlockUsageLocator(package_id="testx.wonderful", block_id="doesnotexist", branch='draft') self.assertFalse(modulestore().has_item(package_id, locator)) # negative tests--insufficient specification self.assertRaises(InsufficientSpecificationError, BlockUsageLocator) - self.assertRaises(InsufficientSpecificationError, - modulestore().has_item, None, BlockUsageLocator(version_guid=self.GUID_D1)) - self.assertRaises(InsufficientSpecificationError, - modulestore().has_item, None, BlockUsageLocator(package_id='GreekHero')) + + locator = CourseLocator(package_id=package_id, branch='draft') + course = modulestore().get_course(locator) + previous_version = course.previous_version + + with self.assertRaises(InsufficientSpecificationError): + modulestore().has_item(None, BlockUsageLocator(version_guid=previous_version)) + with self.assertRaises(InsufficientSpecificationError): + modulestore().has_item(None, BlockUsageLocator(package_id='testx.GreekHero')) def test_get_item(self): ''' get_item(blocklocator) ''' + locator = CourseLocator(package_id="testx.GreekHero", branch='draft') + course = modulestore().get_course(locator) + previous_version = course.previous_version + # positive tests of various forms - locator = BlockUsageLocator(version_guid=self.GUID_D1, block_id='head12345') + locator = BlockUsageLocator(version_guid=previous_version, block_id='head12345') block = modulestore().get_item(locator) self.assertIsInstance(block, CourseDescriptor) # get_instance just redirects to get_item, ignores package_id self.assertIsInstance(modulestore().get_instance("package_id", locator), CourseDescriptor) def verify_greek_hero(block): - self.assertEqual(block.location.package_id, "GreekHero") + """ + Check contents of block + """ + self.assertEqual(block.location.package_id, "testx.GreekHero") self.assertEqual(len(block.tabs), 6, "wrong number of tabs") self.assertEqual(block.display_name, "The Ancient Greek Hero") self.assertEqual(block.advertised_start, "Fall 2013") self.assertEqual(len(block.children), 3) - self.assertEqual(str(block.definition_locator.definition_id), "ad00000000000000dddd0000") # check dates and graders--forces loading of descriptor self.assertEqual(block.edited_by, "testassist@edx.org") self.assertDictEqual( block.grade_cutoffs, {"Pass": 0.45}, ) - locator = BlockUsageLocator(package_id='GreekHero', block_id='head12345', branch='draft') + locator = BlockUsageLocator(package_id='testx.GreekHero', block_id='head12345', branch='draft') verify_greek_hero(modulestore().get_item(locator)) # get_instance just redirects to get_item, ignores package_id verify_greek_hero(modulestore().get_instance("package_id", locator)) @@ -382,16 +762,15 @@ class SplitModuleItemTests(SplitModuleTest): def test_get_non_root(self): # not a course obj - locator = BlockUsageLocator(package_id='GreekHero', block_id='chapter1', branch='draft') + locator = BlockUsageLocator(package_id='testx.GreekHero', block_id='chapter1', branch='draft') block = modulestore().get_item(locator) - self.assertEqual(block.location.package_id, "GreekHero") + self.assertEqual(block.location.package_id, "testx.GreekHero") self.assertEqual(block.category, 'chapter') - self.assertEqual(str(block.definition_locator.definition_id), "cd00000000000000dddd0020") self.assertEqual(block.display_name, "Hercules") self.assertEqual(block.edited_by, "testassist@edx.org") # in published course - locator = BlockUsageLocator(package_id="wonderful", block_id="head23456", branch='published') + locator = BlockUsageLocator(package_id="testx.wonderful", block_id="head23456", branch='published') self.assertIsInstance( modulestore().get_item(locator), CourseDescriptor @@ -402,15 +781,15 @@ class SplitModuleItemTests(SplitModuleTest): locator = BlockUsageLocator(package_id="doesnotexist", block_id="head23456", branch='draft') with self.assertRaises(ItemNotFoundError): modulestore().get_item(locator) - locator = BlockUsageLocator(package_id="wonderful", block_id="doesnotexist", branch='draft') + locator = BlockUsageLocator(package_id="testx.wonderful", block_id="doesnotexist", branch='draft') with self.assertRaises(ItemNotFoundError): modulestore().get_item(locator) # negative tests--insufficient specification with self.assertRaises(InsufficientSpecificationError): - modulestore().get_item(BlockUsageLocator(version_guid=self.GUID_D1)) + modulestore().get_item(BlockUsageLocator(version_guid=ObjectId())) with self.assertRaises(InsufficientSpecificationError): - modulestore().get_item(BlockUsageLocator(package_id='GreekHero', branch='draft')) + modulestore().get_item(BlockUsageLocator(package_id='testx.GreekHero', branch='draft')) # pylint: disable=W0212 def test_matching(self): @@ -443,7 +822,7 @@ class SplitModuleItemTests(SplitModuleTest): ''' get_items(locator, qualifiers, [branch]) ''' - locator = CourseLocator(version_guid=self.GUID_D0) + locator = CourseLocator(package_id="testx.GreekHero", branch='draft') # get all modules matches = modulestore().get_items(locator) self.assertEqual(len(matches), 6) @@ -471,11 +850,11 @@ class SplitModuleItemTests(SplitModuleTest): ''' get_parent_locations(locator, [block_id], [branch]): [BlockUsageLocator] ''' - locator = BlockUsageLocator(package_id="GreekHero", branch='draft', block_id='chapter1') + locator = BlockUsageLocator(package_id="testx.GreekHero", branch='draft', block_id='chapter1') parents = modulestore().get_parent_locations(locator) self.assertEqual(len(parents), 1) self.assertEqual(parents[0].block_id, 'head12345') - self.assertEqual(parents[0].package_id, "GreekHero") + self.assertEqual(parents[0].package_id, "testx.GreekHero") locator.block_id = 'chapter2' parents = modulestore().get_parent_locations(locator) self.assertEqual(len(parents), 1) @@ -488,7 +867,7 @@ class SplitModuleItemTests(SplitModuleTest): """ Test the existing get_children method on xdescriptors """ - locator = BlockUsageLocator(package_id="GreekHero", block_id="head12345", branch='draft') + locator = BlockUsageLocator(package_id="testx.GreekHero", block_id="head12345", branch='draft') block = modulestore().get_item(locator) children = block.get_children() expected_ids = [ @@ -530,9 +909,9 @@ class TestItemCrud(SplitModuleTest): create_item(course_or_parent_locator, category, user, definition_locator=None, fields): new_desciptor """ # grab link to course to ensure new versioning works - locator = CourseLocator(package_id="GreekHero", branch='draft') + locator = CourseLocator(package_id="testx.GreekHero", branch='draft') premod_course = modulestore().get_course(locator) - premod_time = datetime.datetime.now(UTC) - datetime.timedelta(seconds=1) + premod_history = modulestore().get_course_history_info(premod_course.location) # add minimal one w/o a parent category = 'sequential' new_module = modulestore().create_item( @@ -540,7 +919,7 @@ class TestItemCrud(SplitModuleTest): fields={'display_name': 'new sequential'} ) # check that course version changed and course's previous is the other one - self.assertEqual(new_module.location.package_id, "GreekHero") + self.assertEqual(new_module.location.package_id, "testx.GreekHero") self.assertNotEqual(new_module.location.version_guid, premod_course.location.version_guid) self.assertIsNone(locator.version_guid, "Version inadvertently filled in") current_course = modulestore().get_course(locator) @@ -548,10 +927,8 @@ class TestItemCrud(SplitModuleTest): history_info = modulestore().get_course_history_info(current_course.location) self.assertEqual(history_info['previous_version'], premod_course.location.version_guid) - self.assertEqual(str(history_info['original_version']), self.GUID_D3) + self.assertEqual(history_info['original_version'], premod_history['original_version']) self.assertEqual(history_info['edited_by'], "user123") - self.assertGreaterEqual(history_info['edited_on'], premod_time) - self.assertLessEqual(history_info['edited_on'], datetime.datetime.now(UTC)) # check block's info: category, definition_locator, and display_name self.assertEqual(new_module.category, 'sequential') self.assertIsNotNone(new_module.definition_locator) @@ -567,19 +944,22 @@ class TestItemCrud(SplitModuleTest): """ Test create_item w/ specifying the parent of the new item """ - locator = BlockUsageLocator(package_id="wonderful", block_id="head23456", branch='draft') + locator = BlockUsageLocator(package_id="testx.GreekHero", branch='draft', block_id='chapter2') + original = modulestore().get_item(locator) + + locator = BlockUsageLocator(package_id="testx.wonderful", block_id="head23456", branch='draft') premod_course = modulestore().get_course(locator) category = 'chapter' new_module = modulestore().create_item( locator, category, 'user123', fields={'display_name': 'new chapter'}, - definition_locator=DefinitionLocator("cd00000000000000dddd0022") + definition_locator=original.definition_locator ) # check that course version changed and course's previous is the other one self.assertNotEqual(new_module.location.version_guid, premod_course.location.version_guid) parent = modulestore().get_item(locator) self.assertIn(new_module.location.block_id, parent.children) - self.assertEqual(str(new_module.definition_locator.definition_id), "cd00000000000000dddd0022") + self.assertEqual(new_module.definition_locator.definition_id, original.definition_locator.definition_id) def test_unique_naming(self): """ @@ -587,9 +967,11 @@ class TestItemCrud(SplitModuleTest): a definition id and new def data that it branches the definition in the db. Actually, this tries to test all create_item features not tested above. """ - locator = BlockUsageLocator(package_id="contender", block_id="head345679", branch='draft') + locator = BlockUsageLocator(package_id="testx.GreekHero", branch='draft', block_id='problem1') + original = modulestore().get_item(locator) + + locator = BlockUsageLocator(package_id="guestx.contender", block_id="head345679", branch='draft') category = 'problem' - premod_time = datetime.datetime.now(UTC) - datetime.timedelta(seconds=1) new_payload = "empty" new_module = modulestore().create_item( locator, category, 'anotheruser', @@ -599,7 +981,7 @@ class TestItemCrud(SplitModuleTest): another_module = modulestore().create_item( locator, category, 'anotheruser', fields={'display_name': 'problem 2', 'data': another_payload}, - definition_locator=DefinitionLocator("0d00000040000000dddd0031"), + definition_locator=original.definition_locator, ) # check that course version changed and course's previous is the other one parent = modulestore().get_item(locator) @@ -613,17 +995,15 @@ class TestItemCrud(SplitModuleTest): self.assertIsNone(new_history['previous_version']) self.assertEqual(new_history['original_version'], new_module.definition_locator.definition_id) self.assertEqual(new_history['edited_by'], "anotheruser") - self.assertLessEqual(new_history['edited_on'], datetime.datetime.now(UTC)) - self.assertGreaterEqual(new_history['edited_on'], premod_time) another_history = modulestore().get_definition_history_info(another_module.definition_locator) - self.assertEqual(str(another_history['previous_version']), '0d00000040000000dddd0031') + self.assertEqual(another_history['previous_version'], original.definition_locator.definition_id) def test_encoded_naming(self): """ Check that using odd characters in block id don't break ability to add and retrieve block. """ - parent_locator = BlockUsageLocator(package_id="contender", block_id="head345679", branch='draft') - chapter_locator = BlockUsageLocator(package_id="contender", block_id="foo.bar_-~:0", branch='draft') + parent_locator = BlockUsageLocator(package_id="guestx.contender", block_id="head345679", branch='draft') + chapter_locator = BlockUsageLocator(package_id="guestx.contender", block_id="foo.bar_-~:0", branch='draft') modulestore().create_item( parent_locator, 'chapter', 'anotheruser', block_id=chapter_locator.block_id, @@ -634,7 +1014,7 @@ class TestItemCrud(SplitModuleTest): self.assertEqual(new_module.location.block_id, "foo.bar_-~:0") # hardcode to ensure BUL init didn't change # now try making that a parent of something new_payload = "empty" - problem_locator = BlockUsageLocator(package_id="contender", block_id="prob.bar_-~:99a", branch='draft') + problem_locator = BlockUsageLocator(package_id="guestx.contender", block_id="prob.bar_-~:99a", branch='draft') modulestore().create_item( chapter_locator, 'problem', 'anotheruser', block_id=problem_locator.block_id, @@ -652,7 +1032,7 @@ class TestItemCrud(SplitModuleTest): """ # start transaction w/ simple creation user = random.getrandbits(32) - new_course = modulestore().create_course('test_org.test_transaction', 'test_org', 'test_transaction', user) + new_course = modulestore().create_course('test_org.test_transaction', 'test_org', user) new_course_locator = new_course.location.as_course_locator() index_history_info = modulestore().get_course_history_info(new_course.location) course_block_prev_version = new_course.previous_version @@ -735,13 +1115,12 @@ class TestItemCrud(SplitModuleTest): """ test updating an items metadata ensuring the definition doesn't version but the course does if it should """ - locator = BlockUsageLocator(package_id="GreekHero", block_id="problem3_2", branch='draft') + locator = BlockUsageLocator(package_id="testx.GreekHero", block_id="problem3_2", branch='draft') problem = modulestore().get_item(locator) pre_def_id = problem.definition_locator.definition_id pre_version_guid = problem.location.version_guid self.assertIsNotNone(pre_def_id) self.assertIsNotNone(pre_version_guid) - premod_time = datetime.datetime.now(UTC) - datetime.timedelta(seconds=1) self.assertNotEqual(problem.max_attempts, 4, "Invalidates rest of test") problem.max_attempts = 4 @@ -764,16 +1143,13 @@ class TestItemCrud(SplitModuleTest): history_info = modulestore().get_course_history_info(current_course.location) self.assertEqual(history_info['previous_version'], pre_version_guid) - self.assertEqual(str(history_info['original_version']), self.GUID_D3) self.assertEqual(history_info['edited_by'], "**replace_user**") - self.assertGreaterEqual(history_info['edited_on'], premod_time) - self.assertLessEqual(history_info['edited_on'], datetime.datetime.now(UTC)) def test_update_children(self): """ test updating an item's children ensuring the definition doesn't version but the course does if it should """ - locator = BlockUsageLocator(package_id="GreekHero", block_id="chapter3", branch='draft') + locator = BlockUsageLocator(package_id="testx.GreekHero", block_id="chapter3", branch='draft') block = modulestore().get_item(locator) pre_def_id = block.definition_locator.definition_id pre_version_guid = block.location.version_guid @@ -799,7 +1175,7 @@ class TestItemCrud(SplitModuleTest): """ test updating an item's definition: ensure it gets versioned as well as the course getting versioned """ - locator = BlockUsageLocator(package_id="GreekHero", block_id="head12345", branch='draft') + locator = BlockUsageLocator(package_id="testx.GreekHero", block_id="head12345", branch='draft') block = modulestore().get_item(locator) pre_def_id = block.definition_locator.definition_id pre_version_guid = block.location.version_guid @@ -816,8 +1192,10 @@ class TestItemCrud(SplitModuleTest): """ Test updating metadata, children, and definition in a single call ensuring all the versioning occurs """ + locator = BlockUsageLocator(package_id="testx.GreekHero", branch='draft', block_id='problem1') + original = modulestore().get_item(locator) # first add 2 children to the course for the update to manipulate - locator = BlockUsageLocator(package_id="contender", block_id="head345679", branch='draft') + locator = BlockUsageLocator(package_id="guestx.contender", block_id="head345679", branch='draft') category = 'problem' new_payload = "empty" modulestore().create_item( @@ -828,7 +1206,7 @@ class TestItemCrud(SplitModuleTest): modulestore().create_item( locator, category, 'test_update_manifold', fields={'display_name': 'problem 2', 'data': another_payload}, - definition_locator=DefinitionLocator("0d00000040000000dddd0031"), + definition_locator=original.definition_locator, ) # pylint: disable=W0212 modulestore()._clear_cache() @@ -884,6 +1262,9 @@ class TestItemCrud(SplitModuleTest): # check subtree def check_subtree(node): + """ + Check contents of subtree recursively + """ if node: node_loc = node.location self.assertFalse(modulestore().has_item(reusable_location.package_id, @@ -901,7 +1282,10 @@ class TestItemCrud(SplitModuleTest): check_subtree(nodes[0]) def create_course_for_deletion(self): - course = modulestore().create_course('nihilx.deletion', 'nihilx', 'deletion', 'deleting_user') + """ + Create a course we can delete + """ + course = modulestore().create_course('nihilx.deletion', 'nihilx', 'deleting_user') root = BlockUsageLocator( package_id=course.location.package_id, block_id=course.location.block_id, @@ -911,6 +1295,9 @@ class TestItemCrud(SplitModuleTest): return modulestore().get_item(root) def create_subtree_for_deletion(self, parent, category_queue): + """ + Create a subtree in the tb deleted course + """ if not category_queue: return node = modulestore().create_item(parent, category_queue[0], 'deleting_user') @@ -921,29 +1308,23 @@ class TestItemCrud(SplitModuleTest): class TestCourseCreation(SplitModuleTest): """ - Test create_course, duh :-) + Test create_course """ def test_simple_creation(self): """ The simplest case but probing all expected results from it. """ # Oddly getting differences of 200nsec - pre_time = datetime.datetime.now(UTC) - datetime.timedelta(milliseconds=1) - new_course = modulestore().create_course('test_org.test_course', 'test_org', 'test_course', 'create_user') + new_course = modulestore().create_course('test_org.test_course', 'test_org', 'create_user') new_locator = new_course.location # check index entry index_info = modulestore().get_course_index_info(new_locator) self.assertEqual(index_info['org'], 'test_org') - self.assertEqual(index_info['prettyid'], 'test_course') - self.assertGreaterEqual(index_info["edited_on"], pre_time) - self.assertLessEqual(index_info["edited_on"], datetime.datetime.now(UTC)) self.assertEqual(index_info['edited_by'], 'create_user') # check structure info structure_info = modulestore().get_course_history_info(new_locator) self.assertEqual(structure_info['original_version'], index_info['versions']['draft']) self.assertIsNone(structure_info['previous_version']) - self.assertGreaterEqual(structure_info["edited_on"], pre_time) - self.assertLessEqual(structure_info["edited_on"], datetime.datetime.now(UTC)) self.assertEqual(structure_info['edited_by'], 'create_user') # check the returned course object self.assertIsInstance(new_course, CourseDescriptor) @@ -959,28 +1340,23 @@ class TestCourseCreation(SplitModuleTest): """ Test making a course which points to an existing draft and published but not making any changes to either. """ - pre_time = datetime.datetime.now(UTC) - original_locator = CourseLocator(package_id="wonderful", branch='draft') + original_locator = CourseLocator(package_id="testx.wonderful", branch='draft') original_index = modulestore().get_course_index_info(original_locator) new_draft = modulestore().create_course( - 'best', 'leech', 'best_course', 'leech_master', + 'best', 'leech', 'leech_master', versions_dict=original_index['versions']) new_draft_locator = new_draft.location self.assertRegexpMatches(new_draft_locator.package_id, 'best') # the edited_by and other meta fields on the new course will be the original author not this one self.assertEqual(new_draft.edited_by, 'test@edx.org') - self.assertLess(new_draft.edited_on, pre_time) self.assertEqual(new_draft_locator.version_guid, original_index['versions']['draft']) # however the edited_by and other meta fields on course_index will be this one new_index = modulestore().get_course_index_info(new_draft_locator) - self.assertGreaterEqual(new_index["edited_on"], pre_time) - self.assertLessEqual(new_index["edited_on"], datetime.datetime.now(UTC)) self.assertEqual(new_index['edited_by'], 'leech_master') new_published_locator = CourseLocator(package_id=new_draft_locator.package_id, branch='published') new_published = modulestore().get_course(new_published_locator) self.assertEqual(new_published.edited_by, 'test@edx.org') - self.assertLess(new_published.edited_on, pre_time) self.assertEqual(new_published.location.version_guid, original_index['versions']['published']) # changing this course will not change the original course @@ -994,12 +1370,9 @@ class TestCourseCreation(SplitModuleTest): self.assertNotEqual(new_index['versions']['draft'], original_index['versions']['draft']) new_draft = modulestore().get_course(new_draft_locator) self.assertEqual(new_item.edited_by, 'leech_master') - self.assertGreaterEqual(new_item.edited_on, pre_time) self.assertNotEqual(new_item.location.version_guid, original_index['versions']['draft']) self.assertNotEqual(new_draft.location.version_guid, original_index['versions']['draft']) structure_info = modulestore().get_course_history_info(new_draft_locator) - self.assertGreaterEqual(structure_info["edited_on"], pre_time) - self.assertLessEqual(structure_info["edited_on"], datetime.datetime.now(UTC)) self.assertEqual(structure_info['edited_by'], 'leech_master') original_course = modulestore().get_course(original_locator) @@ -1015,8 +1388,7 @@ class TestCourseCreation(SplitModuleTest): """ Create a new course which overrides metadata and course_data """ - pre_time = datetime.datetime.now(UTC) - original_locator = CourseLocator(package_id="contender", branch='draft') + original_locator = CourseLocator(package_id="guestx.contender", branch='draft') original = modulestore().get_course(original_locator) original_index = modulestore().get_course_index_info(original_locator) fields = {} @@ -1028,7 +1400,7 @@ class TestCourseCreation(SplitModuleTest): fields['grading_policy']['GRADE_CUTOFFS'] = {'A': .9, 'B': .8, 'C': .65} fields['display_name'] = 'Derivative' new_draft = modulestore().create_course( - 'counter', 'leech', 'derivative', 'leech_master', + 'counter', 'leech', 'leech_master', versions_dict={'draft': original_index['versions']['draft']}, fields=fields ) @@ -1036,12 +1408,9 @@ class TestCourseCreation(SplitModuleTest): self.assertRegexpMatches(new_draft_locator.package_id, 'counter') # the edited_by and other meta fields on the new course will be the original author not this one self.assertEqual(new_draft.edited_by, 'leech_master') - self.assertGreaterEqual(new_draft.edited_on, pre_time) self.assertNotEqual(new_draft_locator.version_guid, original_index['versions']['draft']) # however the edited_by and other meta fields on course_index will be this one new_index = modulestore().get_course_index_info(new_draft_locator) - self.assertGreaterEqual(new_index["edited_on"], pre_time) - self.assertLessEqual(new_index["edited_on"], datetime.datetime.now(UTC)) self.assertEqual(new_index['edited_by'], 'leech_master') self.assertEqual(new_draft.display_name, fields['display_name']) self.assertDictEqual( @@ -1053,7 +1422,7 @@ class TestCourseCreation(SplitModuleTest): """ Test changing the org, pretty id, etc of a course. Test that it doesn't allow changing the id, etc. """ - locator = CourseLocator(package_id="GreekHero", branch='draft') + locator = CourseLocator(package_id="testx.GreekHero", branch='draft') course_info = modulestore().get_course_index_info(locator) course_info['org'] = 'funkyU' modulestore().update_course_index(course_info) @@ -1061,24 +1430,23 @@ class TestCourseCreation(SplitModuleTest): self.assertEqual(course_info['org'], 'funkyU') course_info['org'] = 'moreFunky' - course_info['prettyid'] = 'Ancient Greek Demagods' modulestore().update_course_index(course_info) course_info = modulestore().get_course_index_info(locator) self.assertEqual(course_info['org'], 'moreFunky') - self.assertEqual(course_info['prettyid'], 'Ancient Greek Demagods') # an allowed but not necessarily recommended way to revert the draft version + head_course = modulestore().get_course(locator) versions = course_info['versions'] - versions['draft'] = self.GUID_D1 + versions['draft'] = head_course.previous_version modulestore().update_course_index(course_info) course = modulestore().get_course(locator) - self.assertEqual(str(course.location.version_guid), self.GUID_D1) + self.assertEqual(course.location.version_guid, versions['draft']) # an allowed but not recommended way to publish a course - versions['published'] = self.GUID_D1 + versions['published'] = versions['draft'] modulestore().update_course_index(course_info) course = modulestore().get_course(CourseLocator(package_id=locator.package_id, branch="published")) - self.assertEqual(str(course.location.version_guid), self.GUID_D1) + self.assertEqual(course.location.version_guid, versions['draft']) def test_create_with_root(self): """ @@ -1086,7 +1454,7 @@ class TestCourseCreation(SplitModuleTest): """ user = random.getrandbits(32) new_course = modulestore().create_course( - 'test_org.test_transaction', 'test_org', 'test_transaction', user, + 'test_org.test_transaction', 'test_org', user, root_block_id='top', root_category='chapter' ) self.assertEqual(new_course.location.block_id, 'top') @@ -1120,11 +1488,11 @@ class TestInheritance(SplitModuleTest): """ # Note, not testing value where defined (course) b/c there's no # defined accessor for it on CourseDescriptor. - locator = BlockUsageLocator(package_id="GreekHero", block_id="problem3_2", branch='draft') + locator = BlockUsageLocator(package_id="testx.GreekHero", block_id="problem3_2", branch='draft') node = modulestore().get_item(locator) # inherited self.assertEqual(node.graceperiod, datetime.timedelta(hours=2)) - locator = BlockUsageLocator(package_id="GreekHero", block_id="problem1", branch='draft') + locator = BlockUsageLocator(package_id="testx.GreekHero", block_id="problem1", branch='draft') node = modulestore().get_item(locator) # overridden self.assertEqual(node.graceperiod, datetime.timedelta(hours=4)) @@ -1139,14 +1507,14 @@ class TestPublish(SplitModuleTest): self.user = random.getrandbits(32) def tearDown(self): - SplitModuleTest.tearDownClass() + SplitModuleTest.tearDown(self) def test_publish_safe(self): """ Test the standard patterns: publish to new branch, revise and publish """ - source_course = CourseLocator(package_id="GreekHero", branch='draft') - dest_course = CourseLocator(package_id="GreekHero", branch="published") + source_course = CourseLocator(package_id="testx.GreekHero", branch='draft') + dest_course = CourseLocator(package_id="testx.GreekHero", branch="published") modulestore().xblock_publish(self.user, source_course, dest_course, ["head12345"], ["chapter2", "chapter3"]) expected = ["head12345", "chapter1"] self._check_course( @@ -1154,19 +1522,19 @@ class TestPublish(SplitModuleTest): ) # add a child under chapter1 new_module = modulestore().create_item( - self._usage(source_course, "chapter1"), "sequential", self.user, + BlockUsageLocator.make_relative(source_course, "chapter1"), "sequential", self.user, fields={'display_name': 'new sequential'}, ) # remove chapter1 from expected b/c its pub'd version != the source anymore since source changed expected.remove("chapter1") # check that it's not in published course with self.assertRaises(ItemNotFoundError): - modulestore().get_item(self._usage(dest_course, new_module.location.block_id)) + modulestore().get_item(BlockUsageLocator.make_relative(dest_course, new_module.location.block_id)) # publish it modulestore().xblock_publish(self.user, source_course, dest_course, [new_module.location.block_id], None) expected.append(new_module.location.block_id) # check that it is in the published course and that its parent is the chapter - pub_module = modulestore().get_item(self._usage(dest_course, new_module.location.block_id)) + pub_module = modulestore().get_item(BlockUsageLocator.make_relative(dest_course, new_module.location.block_id)) self.assertEqual( modulestore().get_parent_locations(pub_module.location)[0].block_id, "chapter1" ) @@ -1178,7 +1546,7 @@ class TestPublish(SplitModuleTest): modulestore().xblock_publish(self.user, source_course, dest_course, [new_module.location.block_id], None) expected.append(new_module.location.block_id) # check that it is in the published course (no error means it worked) - pub_module = modulestore().get_item(self._usage(dest_course, new_module.location.block_id)) + pub_module = modulestore().get_item(BlockUsageLocator.make_relative(dest_course, new_module.location.block_id)) self._check_course( source_course, dest_course, expected, ["chapter2", "chapter3", "problem1", "problem3_2"] ) @@ -1187,13 +1555,13 @@ class TestPublish(SplitModuleTest): """ Test the exceptions which preclude successful publication """ - source_course = CourseLocator(package_id="GreekHero", branch='draft') + source_course = CourseLocator(package_id="testx.GreekHero", branch='draft') # destination does not exist destination_course = CourseLocator(package_id="Unknown", branch="published") with self.assertRaises(ItemNotFoundError): modulestore().xblock_publish(self.user, source_course, destination_course, ["chapter3"], None) # publishing into a new branch w/o publishing the root - destination_course = CourseLocator(package_id="GreekHero", branch="published") + destination_course = CourseLocator(package_id="testx.GreekHero", branch="published") with self.assertRaises(ItemNotFoundError): modulestore().xblock_publish(self.user, source_course, destination_course, ["chapter3"], None) # publishing a subdag w/o the parent already in course @@ -1205,17 +1573,17 @@ class TestPublish(SplitModuleTest): """ Test publishing moves and deletes. """ - source_course = CourseLocator(package_id="GreekHero", branch='draft') - dest_course = CourseLocator(package_id="GreekHero", branch="published") + source_course = CourseLocator(package_id="testx.GreekHero", branch='draft') + dest_course = CourseLocator(package_id="testx.GreekHero", branch="published") modulestore().xblock_publish(self.user, source_course, dest_course, ["head12345"], ["chapter2"]) expected = ["head12345", "chapter1", "chapter3", "problem1", "problem3_2"] self._check_course(source_course, dest_course, expected, ["chapter2"]) # now move problem1 and delete problem3_2 - chapter1 = modulestore().get_item(self._usage(source_course, "chapter1")) - chapter3 = modulestore().get_item(self._usage(source_course, "chapter3")) + chapter1 = modulestore().get_item(BlockUsageLocator.make_relative(source_course, "chapter1")) + chapter3 = modulestore().get_item(BlockUsageLocator.make_relative(source_course, "chapter3")) chapter1.children.append("problem1") chapter3.children.remove("problem1") - modulestore().delete_item(self._usage(source_course, "problem3_2"), self.user) + modulestore().delete_item(BlockUsageLocator.make_relative(source_course, "problem3_2"), self.user) modulestore().xblock_publish(self.user, source_course, dest_course, ["head12345"], ["chapter2"]) expected = ["head12345", "chapter1", "chapter3", "problem1"] self._check_course(source_course, dest_course, expected, ["chapter2", "problem3_2"]) @@ -1224,13 +1592,18 @@ class TestPublish(SplitModuleTest): """ Check that the course has the expected blocks and does not have the unexpected blocks """ + history_info = modulestore().get_course_history_info(dest_course_loc) + self.assertEqual(history_info['edited_by'], self.user) for expected in expected_blocks: - source = modulestore().get_item(self._usage(source_course_loc, expected)) - pub_copy = modulestore().get_item(self._usage(dest_course_loc, expected)) + source = modulestore().get_item(BlockUsageLocator.make_relative(source_course_loc, expected)) + pub_copy = modulestore().get_item(BlockUsageLocator.make_relative(dest_course_loc, expected)) # everything except previous_version & children should be the same self.assertEqual(source.category, pub_copy.category) self.assertEqual(source.update_version, pub_copy.update_version) - self.assertEqual(self.user, pub_copy.edited_by) + self.assertEqual( + self.user, pub_copy.edited_by, + "{} edited_by {} not {}".format(pub_copy.location, pub_copy.edited_by, self.user) + ) for field in source.fields.values(): if field.name == 'children': self._compare_children(field.read_from(source), field.read_from(pub_copy), unexpected_blocks) @@ -1238,13 +1611,7 @@ class TestPublish(SplitModuleTest): self.assertEqual(field.read_from(source), field.read_from(pub_copy)) for unexp in unexpected_blocks: with self.assertRaises(ItemNotFoundError): - modulestore().get_item(self._usage(dest_course_loc, unexp)) - - def _usage(self, course_loc, block_id): - """ - Generate a BlockUsageLocator for the combo of the course location and block id - """ - return BlockUsageLocator(package_id=course_loc.package_id, branch=course_loc.branch, block_id=block_id) + modulestore().get_item(BlockUsageLocator.make_relative(dest_course_loc, unexp)) def _compare_children(self, source_children, dest_children, unexpected): """ @@ -1259,17 +1626,40 @@ class TestPublish(SplitModuleTest): dest_cursor += 1 self.assertEqual(dest_cursor, len(dest_children)) +class TestSchema(SplitModuleTest): + """ + Test the db schema (and possibly eventually migrations?) + """ + def test_schema(self): + """ + Test that the schema is set in each document + """ + db_connection = modulestore().db_connection + for collection in [db_connection.course_index, db_connection.structures, db_connection.definitions]: + self.assertEqual( + collection.find({'schema_version': {'$exists': False}}).count(), + 0, + "{0.name} has records without schema_version".format(collection) + ) + self.assertEqual( + collection.find({'schema_version': {'$ne': SplitMongoModuleStore.SCHEMA_VERSION}}).count(), + 0, + "{0.name} has records with wrong schema_version".format(collection) + ) #=========================================== -# This mocks the django.modulestore() function and is intended purely to disentangle -# the tests from django def modulestore(): + """ + Mock the django dependent global modulestore function to disentangle tests from django + """ def load_function(engine_path): + """ + Load the given engine + """ module_path, _, name = engine_path.rpartition('.') return getattr(import_module(module_path), name) if SplitModuleTest.modulestore is None: - SplitModuleTest.bootstrapDB() class_ = load_function(SplitModuleTest.MODULESTORE['ENGINE']) options = {} @@ -1283,6 +1673,8 @@ def modulestore(): **options ) + SplitModuleTest.bootstrapDB() + return SplitModuleTest.modulestore diff --git a/common/test/data/splitmongo_json/active_versions.json b/common/test/data/splitmongo_json/active_versions.json deleted file mode 100644 index b41440e0e7..0000000000 --- a/common/test/data/splitmongo_json/active_versions.json +++ /dev/null @@ -1,27 +0,0 @@ -[{"_id" : "GreekHero", - "org" : "testx", - "prettyid" : "test_course", - "versions" : { - "draft" : { "$oid" : "1d00000000000000dddd0000" } - }, - "edited_on" : {"$date" : 1364481713238}, - "edited_by" : "test@edx.org"}, - - {"_id" : "wonderful", - "org" : "testx", - "prettyid" : "another_course", - "versions" : { - "draft" : { "$oid" : "1d00000000000000dddd2222" }, - "published" : { "$oid" : "1d00000000000000eeee0000" } - }, - "edited_on" : {"$date" : 1364481313238}, - "edited_by" : "test@edx.org"}, - - {"_id" : "contender", - "org" : "guestx", - "prettyid" : "test_course", - "versions" : { - "draft" : { "$oid" : "1d00000000000000dddd5555" }}, - "edited_on" : {"$date" : 1364491313238}, - "edited_by" : "test@guestx.edu"} -] diff --git a/common/test/data/splitmongo_json/definitions.json b/common/test/data/splitmongo_json/definitions.json deleted file mode 100644 index 81b84fe951..0000000000 --- a/common/test/data/splitmongo_json/definitions.json +++ /dev/null @@ -1,356 +0,0 @@ -[ - { - "_id": { "$oid" : "ad00000000000000dddd0000"}, - "category":"course", - "fields":{ - "textbooks":[ - - ], - "grading_policy":{ - "GRADER":[ - { - "min_count":4, - "weight":0.15, - "type":"Homework", - "drop_count":2, - "short_label":"HWa" - }, - { - "short_label":"", - "min_count":12, - "type":"Lab", - "drop_count":2, - "weight":0.15 - }, - { - "short_label":"Midterm", - "min_count":1, - "type":"Midterm Exam", - "drop_count":0, - "weight":0.3 - }, - { - "short_label":"Final", - "min_count":1, - "type":"Final Exam", - "drop_count":0, - "weight":0.4 - } - ], - "GRADE_CUTOFFS":{ - "Pass":0.45 - } - }, - "wiki_slug":null - }, - "edit_info": { - "edited_by":"testassist@edx.org", - "edited_on":{"$date" : 1364481713238}, - "previous_version":{ "$oid" : "ad00000000000000dddd0001"}, - "original_version":{ "$oid" : "ad00000000000000dddd0010"} - } - }, - { - "_id":{ "$oid" : "ad00000000000000dddd0001"}, - "category":"course", - "fields":{ - "textbooks":[ - - ], - "grading_policy":{ - "GRADER":[ - { - "min_count":5, - "weight":0.15, - "type":"Homework", - "drop_count":1, - "short_label":"HWa" - }, - { - "short_label":"", - "min_count":12, - "type":"Lab", - "drop_count":2, - "weight":0.15 - }, - { - "short_label":"Midterm", - "min_count":1, - "type":"Midterm Exam", - "drop_count":0, - "weight":0.3 - }, - { - "short_label":"Final", - "min_count":1, - "type":"Final Exam", - "drop_count":0, - "weight":0.4 - } - ], - "GRADE_CUTOFFS":{ - "Pass":0.55 - } - }, - "wiki_slug":null - }, - "edit_info": { - "edited_by":"testassist@edx.org", - "edited_on":{"$date" : 1364481713238}, - "previous_version":{ "$oid" : "ad00000000000000dddd0010"}, - "original_version":{ "$oid" : "ad00000000000000dddd0010"} - } - }, - { - "_id":{ "$oid" : "ad00000000000000dddd0010"}, - "category":"course", - "fields":{ - "textbooks":[ - - ], - "grading_policy":{ - "GRADER":[ - { - "min_count":5, - "weight":0.15, - "type":"Homework", - "drop_count":1, - "short_label":"HWa" - }, - { - "short_label":"", - "min_count":2, - "type":"Lab", - "drop_count":0, - "weight":0.15 - }, - { - "short_label":"Midterm", - "min_count":1, - "type":"Midterm Exam", - "drop_count":0, - "weight":0.3 - }, - { - "short_label":"Final", - "min_count":1, - "type":"Final Exam", - "drop_count":0, - "weight":0.4 - } - ], - "GRADE_CUTOFFS":{ - "Pass":0.75 - } - }, - "wiki_slug":null - }, - "edit_info": { - "edited_by":"test@edx.org", - "edited_on":{"$date": 1364473713238}, - "previous_version":null, - "original_version":{ "$oid" : "ad00000000000000dddd0010"} - } - }, - { - "_id":{ "$oid" : "ad00000000000000dddd0020"}, - "category":"course", - "fields":{ - "textbooks":[ - - ], - "grading_policy":{ - "GRADER":[ - { - "min_count":14, - "weight":0.25, - "type":"Homework", - "drop_count":1, - "short_label":"HWa" - }, - { - "short_label":"", - "min_count":12, - "type":"Lab", - "drop_count":2, - "weight":0.25 - }, - { - "short_label":"Midterm", - "min_count":1, - "type":"Midterm Exam", - "drop_count":0, - "weight":0.2 - }, - { - "short_label":"Final", - "min_count":1, - "type":"Final Exam", - "drop_count":0, - "weight":0.3 - } - ], - "GRADE_CUTOFFS":{ - "Pass":0.45 - } - }, - "wiki_slug":null - }, - "edit_info": { - "edited_by":"test@edx.org", - "edited_on":{"$date": 1364481313238}, - "previous_version":{ "$oid" : "2d00000000000000dddd0020"}, - "original_version":{ "$oid" : "2d00000000000000dddd0020"} - } - }, - { - "_id":{ "$oid" : "2d00000000000000dddd0020"}, - "category":"course", - "fields":{ - "textbooks":[ - - ], - "grading_policy":{ - "GRADER":[ - { - "min_count":14, - "weight":0.25, - "type":"Homework", - "drop_count":1, - "short_label":"HWa" - }, - { - "short_label":"", - "min_count":12, - "type":"Lab", - "drop_count":2, - "weight":0.25 - }, - { - "short_label":"Midterm", - "min_count":1, - "type":"Midterm Exam", - "drop_count":0, - "weight":0.2 - }, - { - "short_label":"Final", - "min_count":1, - "type":"Final Exam", - "drop_count":0, - "weight":0.3 - } - ], - "GRADE_CUTOFFS":{ - "Pass":0.95 - } - }, - "wiki_slug":null - }, - "edit_info": { - "edited_by":"test@edx.org", - "edited_on":{"$date" : 1364481313238}, - "previous_version":null, - "original_version":{ "$oid" : "2d00000000000000dddd0020"} - } - }, - { - "_id":{ "$oid" : "3d00000000000000dddd0020"}, - "category":"course", - "fields":{ - "textbooks":[ - - ], - "grading_policy":{ - "GRADER":[ - { - "min_count":4, - "weight":0.25, - "type":"Homework", - "drop_count":0, - "short_label":"HW" - }, - { - "short_label":"Midterm", - "min_count":1, - "type":"Midterm Exam", - "drop_count":0, - "weight":0.4 - }, - { - "short_label":"Final", - "min_count":1, - "type":"Final Exam", - "drop_count":0, - "weight":0.35 - } - ], - "GRADE_CUTOFFS":{ - "Pass":0.25 - } - }, - "wiki_slug":null - }, - "edit_info": { - "edited_by":"test@edx.org", - "edited_on":{"$date" : 1364481313238}, - "previous_version":null, - "original_version":{ "$oid" : "2d00000000000000dddd0020"} - } - }, - { - "_id":{ "$oid" : "cd00000000000000dddd0020"}, - "category":"chapter", - "fields":{}, - "edit_info": { - "edited_by":"testassist@edx.org", - "edited_on":{"$date" : 1364483713238}, - "previous_version":null, - "original_version":{ "$oid" : "cd00000000000000dddd0020"} - } - }, - { - "_id":{ "$oid" : "cd00000000000000dddd0022"}, - "category":"chapter", - "fields":{}, - "edit_info": { - "edited_by":"testassist@edx.org", - "edited_on":{"$date" : 1364483713238}, - "previous_version":null, - "original_version":{ "$oid" : "cd00000000000000dddd0022"} - } - }, - { - "_id":{ "$oid" : "cd00000000000000dddd0032"}, - "category":"chapter", - "fields":{}, - "edit_info": { - "edited_by":"testassist@edx.org", - "edited_on":{"$date" : 1364483713238}, - "previous_version":null, - "original_version":{ "$oid" : "cd00000000000000dddd0032"} - } - }, - { - "_id":{ "$oid" : "0d00000040000000dddd0031"}, - "category":"problem", - "fields": {"data": ""}, - "edit_info": { - "edited_by":"testassist@edx.org", - "edited_on":{"$date" : 1364483713238}, - "previous_version":null, - "original_version":{ "$oid" : "0d00000040000000dddd0031"} - } - }, - { - "_id":{ "$oid" : "0d00000040000000dddd0032"}, - "category":"problem", - "fields": {"data": ""}, - "edit_info": { - "edited_by":"testassist@edx.org", - "edited_on":{"$date" : 1364483713238}, - "previous_version":null, - "original_version":{ "$oid" : "0d00000040000000dddd0032"} - } - } -] \ No newline at end of file diff --git a/common/test/data/splitmongo_json/structures.json b/common/test/data/splitmongo_json/structures.json deleted file mode 100644 index 644df803b4..0000000000 --- a/common/test/data/splitmongo_json/structures.json +++ /dev/null @@ -1,495 +0,0 @@ -[ - { - "_id": { "$oid" : "1d00000000000000dddd0000"}, - "root":"head12345", - "original_version":{ "$oid" : "1d00000000000000dddd3333" }, - "previous_version":{ "$oid" : "1d00000000000000dddd1111" }, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364483713238 - }, - "blocks":{ - "head12345":{ - "category":"course", - "definition":{ "$oid" : "ad00000000000000dddd0000"}, - "fields":{ - "children":[ - "chapter1", - "chapter2", - "chapter3" - ], - "end":"2013-06-13T04:30", - "tabs":[ - { - "type":"courseware" - }, - { - "type":"course_info", - "name":"Course Info" - }, - { - "type":"discussion", - "name":"Discussion" - }, - { - "type":"wiki", - "name":"Wiki" - }, - { - "type":"static_tab", - "name":"Syllabus", - "url_slug":"01356a17b5924b17a04b7fc2426a3798" - }, - { - "type":"static_tab", - "name":"Advice for Students", - "url_slug":"57e9991c0d794ff58f7defae3e042e39" - } - ], - "enrollment_start":"2013-01-01T05:00", - "graceperiod":"2 hours 0 minutes 0 seconds", - "start":"2013-02-14T05:00", - "enrollment_end":"2013-03-02T05:00", - "data_dir":"MITx-2-Base", - "advertised_start":"Fall 2013", - "display_name":"The Ancient Greek Hero" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd0000" }, - "previous_version":{ "$oid" : "1d00000000000000dddd1111" }, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364483713238 - } - } - }, - "chapter1":{ - "category":"chapter", - "definition":{ "$oid" : "cd00000000000000dddd0020"}, - "fields":{ - "children":[ - - ], - "display_name":"Hercules" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd0000" }, - "previous_version":null, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364483713238 - } - } - }, - "chapter2":{ - "category":"chapter", - "definition":{ "$oid" : "cd00000000000000dddd0022"}, - "fields":{ - "children":[ - - ], - "display_name":"Hera heckles Hercules" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd0000" }, - "previous_version":null, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364483713238 - } - } - }, - "chapter3":{ - "category":"chapter", - "definition":{ "$oid" : "cd00000000000000dddd0032"}, - "fields":{ - "children":[ - "problem1", - "problem3_2" - ], - "display_name":"Hera cuckolds Zeus" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd0000" }, - "previous_version":null, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364483713238 - } - } - }, - "problem1":{ - "category":"problem", - "definition":{ "$oid" : "0d00000040000000dddd0031"}, - "fields":{ - "children":[ - - ], - "display_name":"Problem 3.1", - "graceperiod":"4 hours 0 minutes 0 seconds" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd0000" }, - "previous_version":null, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364483713238 - } - } - }, - "problem3_2":{ - "category":"problem", - "definition":{ "$oid" : "0d00000040000000dddd0032"}, - "fields":{ - "children":[ - - ], - "display_name":"Problem 3.2" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd0000" }, - "previous_version":null, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364483713238 - } - } - } - } - }, - { - "_id": { "$oid" : "1d00000000000000dddd1111"}, - "root":"head12345", - "original_version":{ "$oid" : "1d00000000000000dddd3333" }, - "previous_version":{ "$oid" : "1d00000000000000dddd3333" }, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364481713238 - }, - "blocks":{ - "head12345":{ - "category":"course", - "definition":{ "$oid" : "ad00000000000000dddd0001"}, - "fields":{ - "children":[ - - ], - "end":"2013-04-13T04:30", - "tabs":[ - { - "type":"courseware" - }, - { - "type":"course_info", - "name":"Course Info" - }, - { - "type":"discussion", - "name":"Discussion" - }, - { - "type":"wiki", - "name":"Wiki" - }, - { - "type":"static_tab", - "name":"Syllabus", - "url_slug":"01356a17b5924b17a04b7fc2426a3798" - }, - { - "type":"static_tab", - "name":"Advice for Students", - "url_slug":"57e9991c0d794ff58f7defae3e042e39" - } - ], - "enrollment_start":null, - "graceperiod":"2 hours 0 minutes 0 seconds", - "start":"2013-02-14T05:00", - "enrollment_end":null, - "data_dir":"MITx-2-Base", - "advertised_start":null, - "display_name":"The Ancient Greek Hero" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd1111" }, - "previous_version":{ "$oid" : "1d00000000000000dddd3333" }, - "edited_by":"testassist@edx.org", - "edited_on":{ - "$date":1364481713238 - } - } - } - } - }, - { - "_id": { "$oid" : "1d00000000000000dddd3333"}, - "root":"head12345", - "original_version":{ "$oid" : "1d00000000000000dddd3333" }, - "previous_version":null, - "edited_by":"test@edx.org", - "edited_on":{ - "$date":1364473713238 - }, - "blocks":{ - "head12345":{ - "category":"course", - "definition":{ "$oid" : "ad00000000000000dddd0010"}, - "fields":{ - "children":[ - - ], - "end":null, - "tabs":[ - { - "type":"courseware" - }, - { - "type":"course_info", - "name":"Course Info" - }, - { - "type":"discussion", - "name":"Discussion" - }, - { - "type":"wiki", - "name":"Wiki" - } - ], - "enrollment_start":null, - "graceperiod":null, - "start":"2013-02-14T05:00", - "enrollment_end":null, - "data_dir":"MITx-2-Base", - "advertised_start":null, - "display_name":"The Ancient Greek Hero" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd3333" }, - "previous_version":null, - "edited_by":"test@edx.org", - "edited_on":{ - "$date":1364473713238 - } - } - } - } - }, - { - "_id": { "$oid" : "1d00000000000000dddd2222"}, - "root":"head23456", - "original_version":{ "$oid" : "1d00000000000000dddd4444" }, - "previous_version":{ "$oid" : "1d00000000000000dddd4444" }, - "edited_by":"test@edx.org", - "edited_on":{ - "$date":1364481313238 - }, - "blocks":{ - "head23456":{ - "category":"course", - "definition":{ "$oid" : "ad00000000000000dddd0020"}, - "fields":{ - "children":[ - - ], - "end":null, - "tabs":[ - { - "type":"courseware" - }, - { - "type":"course_info", - "name":"Course Info" - }, - { - "type":"discussion", - "name":"Discussion" - }, - { - "type":"wiki", - "name":"Wiki" - } - ], - "enrollment_start":null, - "graceperiod":null, - "start":"2013-02-14T05:00", - "enrollment_end":null, - "data_dir":"MITx-2-Base", - "advertised_start":null, - "display_name":"The most wonderful course" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd2222" }, - "previous_version":{ "$oid" : "1d00000000000000dddd4444" }, - "edited_by":"test@edx.org", - "edited_on":{ - "$date":1364481313238 - } - } - - } - } - }, - { - "_id": { "$oid" : "1d00000000000000dddd4444"}, - "root":"head23456", - "original_version":{ "$oid" : "1d00000000000000dddd4444" }, - "previous_version":null, - "edited_by":"test@edx.org", - "edited_on":{ - "$date":1364480313238 - }, - "blocks":{ - "head23456":{ - "category":"course", - "definition":{ "$oid" : "2d00000000000000dddd0020"}, - "fields":{ - "children":[ - - ], - "end":null, - "tabs":[ - { - "type":"courseware" - }, - { - "type":"course_info", - "name":"Course Info" - }, - { - "type":"discussion", - "name":"Discussion" - }, - { - "type":"wiki", - "name":"Wiki" - } - ], - "enrollment_start":null, - "graceperiod":null, - "start":"2013-02-14T05:00", - "enrollment_end":null, - "data_dir":"MITx-2-Base", - "advertised_start":null, - "display_name":"A wonderful course" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd4444" }, - "previous_version":null, - "edited_by":"test@edx.org", - "edited_on":{ - "$date":1364480313238 - } - } - } - } - }, - { - "_id": { "$oid" : "1d00000000000000eeee0000"}, - "root":"head23456", - "original_version":{ "$oid" : "1d00000000000000eeee0000" }, - "previous_version":null, - "edited_by":"test@edx.org", - "edited_on":{ - "$date":1364481333238 - }, - "blocks":{ - "head23456":{ - "category":"course", - "definition":{ "$oid" : "ad00000000000000dddd0020"}, - "fields":{ - "children":[ - - ], - "end":null, - "tabs":[ - { - "type":"courseware" - }, - { - "type":"course_info", - "name":"Course Info" - }, - { - "type":"discussion", - "name":"Discussion" - }, - { - "type":"wiki", - "name":"Wiki" - } - ], - "enrollment_start":null, - "graceperiod":null, - "start":"2013-02-14T05:00", - "enrollment_end":null, - "data_dir":"MITx-2-Base", - "advertised_start":null, - "display_name":"The most wonderful course" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000eeee0000" }, - "previous_version":null, - "edited_by":"test@edx.org", - "edited_on":{ - "$date":1364481333238 - } - } - } - } - }, - { - "_id": { "$oid" : "1d00000000000000dddd5555"}, - "root":"head345679", - "original_version":{ "$oid" : "1d00000000000000dddd5555" }, - "previous_version":null, - "edited_by":"test@guestx.edu", - "edited_on":{ - "$date":1364491313238 - }, - "blocks":{ - "head345679":{ - "category":"course", - "definition":{ "$oid" : "3d00000000000000dddd0020"}, - "fields":{ - "children":[ - - ], - "end":null, - "tabs":[ - { - "type":"courseware" - }, - { - "type":"course_info", - "name":"Course Info" - }, - { - "type":"discussion", - "name":"Discussion" - }, - { - "type":"wiki", - "name":"Wiki" - } - ], - "enrollment_start":null, - "graceperiod":null, - "start":"2013-03-14T05:00", - "enrollment_end":null, - "data_dir":"MITx-3-Base", - "advertised_start":null, - "display_name":"Yet another contender" - }, - "edit_info": { - "update_version":{ "$oid" : "1d00000000000000dddd5555" }, - "previous_version":null, - "edited_by":"test@guestx.edu", - "edited_on":{ - "$date":1364491313238 - } - } - } - } - } -]