Commit 437a3bf9 by Don Mitchell

Merge pull request #4542 from edx/split/perf_test

Split/perf test
parents 36d47dd0 02e21d8a
......@@ -8,8 +8,6 @@ from datetime import datetime
import dateutil.parser
from lazy import lazy
from opaque_keys.edx.locations import Location
from opaque_keys.edx.locator import UsageKey
from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.graders import grader_from_conf
from xmodule.tabs import CourseTabList
......@@ -17,7 +15,6 @@ import json
from xblock.fields import Scope, List, String, Dict, Boolean, Integer
from .fields import Date
from opaque_keys.edx.locator import CourseLocator
from django.utils.timezone import UTC
log = logging.getLogger(__name__)
......
......@@ -568,10 +568,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
:param category: the xblock category
:param fields: the dictionary of {fieldname: value}
"""
result = collections.defaultdict(dict)
if fields is None:
return {}
return result
cls = self.mixologist.mix(XBlock.load_class(category, select=prefer_xmodules))
result = collections.defaultdict(dict)
for field_name, value in fields.iteritems():
field = getattr(cls, field_name)
result[field.scope][field_name] = value
......
......@@ -85,3 +85,6 @@ class InvalidBranchSetting(Exception):
super(InvalidBranchSetting, self).__init__()
self.expected_setting = expected_setting
self.actual_setting = actual_setting
def __unicode__(self, *args, **kwargs):
return u"Invalid branch: expected {} but got {}".format(self.expected_setting, self.actual_setting)
......@@ -95,7 +95,7 @@ def path_to_location(modulestore, usage_key):
category = path[path_index].block_type
if category == 'sequential' or category == 'videosequence':
section_desc = modulestore.get_item(path[path_index])
child_locs = [c.location for c in section_desc.get_children()]
child_locs = [c.location.version_agnostic() for c in section_desc.get_children()]
# positions are 1-indexed, and should be strings to be consistent with
# url parsing.
position_list.append(str(child_locs.index(path[path_index + 1]) + 1))
......
......@@ -53,7 +53,7 @@ class SplitMigrator(object):
new_run = source_course_key.run
new_course_key = CourseLocator(new_org, new_course, new_run, branch=ModuleStoreEnum.BranchName.published)
new_fields = self._get_json_fields_translate_references(original_course, new_course_key, None)
new_fields = self._get_fields_translate_references(original_course, new_course_key, None)
if fields:
new_fields.update(fields)
new_course = self.split_modulestore.create_course(
......@@ -92,7 +92,7 @@ class SplitMigrator(object):
course_version_locator,
module.location.block_type,
block_id=module.location.block_id,
fields=self._get_json_fields_translate_references(
fields=self._get_fields_translate_references(
module, course_version_locator, new_course.location.block_id
),
continue_version=True
......@@ -127,7 +127,7 @@ class SplitMigrator(object):
if field.is_set_on(split_module) and not module.fields[name].is_set_on(module):
field.delete_from(split_module)
for field, value in self._get_fields_translate_references(
module, new_draft_course_loc, published_course_usage_key.block_id
module, new_draft_course_loc, published_course_usage_key.block_id, field_names=False
).iteritems():
field.write_to(split_module, value)
......@@ -138,7 +138,7 @@ class SplitMigrator(object):
user_id, new_draft_course_loc,
new_locator.block_type,
block_id=new_locator.block_id,
fields=self._get_json_fields_translate_references(
fields=self._get_fields_translate_references(
module, new_draft_course_loc, published_course_usage_key.block_id
)
)
......@@ -173,43 +173,13 @@ class SplitMigrator(object):
new_parent.children.insert(new_parent_cursor, new_locator)
new_parent = self.split_modulestore.update_item(new_parent, user_id)
def _get_json_fields_translate_references(self, xblock, new_course_key, course_block_id):
"""
Return the json repr for explicitly set fields but convert all references to their Locators
"""
def get_translation(location):
"""
Convert the location
"""
return new_course_key.make_usage_key(
location.category,
location.block_id if location.category != 'course' else course_block_id
)
result = {}
for field_name, field in xblock.fields.iteritems():
if field.is_set_on(xblock):
field_value = getattr(xblock, field_name)
if isinstance(field, Reference) and field_value is not None:
result[field_name] = get_translation(field_value)
elif isinstance(field, ReferenceList):
result[field_name] = [
get_translation(ele) for ele in field_value
]
elif isinstance(field, ReferenceValueDict):
result[field_name] = {
key: get_translation(subvalue)
for key, subvalue in field_value.iteritems()
}
else:
result[field_name] = field.read_json(xblock)
return result
def _get_fields_translate_references(self, xblock, new_course_key, course_block_id):
def _get_fields_translate_references(self, xblock, new_course_key, course_block_id, field_names=True):
"""
Return a dictionary of field: value pairs for explicitly set fields
but convert all references to their BlockUsageLocators
Args:
field_names: if Truthy, the dictionary keys are the field names. If falsey, the keys are the
field objects.
"""
def get_translation(location):
"""
......@@ -223,19 +193,20 @@ class SplitMigrator(object):
result = {}
for field_name, field in xblock.fields.iteritems():
if field.is_set_on(xblock):
field_value = getattr(xblock, field_name)
field_value = field.read_from(xblock)
field_key = field_name if field_names else field
if isinstance(field, Reference) and field_value is not None:
result[field] = get_translation(field_value)
result[field_key] = get_translation(field_value)
elif isinstance(field, ReferenceList):
result[field] = [
result[field_key] = [
get_translation(ele) for ele in field_value
]
elif isinstance(field, ReferenceValueDict):
result[field] = {
result[field_key] = {
key: get_translation(subvalue)
for key, subvalue in field_value.iteritems()
}
else:
result[field] = field_value
result[field_key] = field_value
return result
......@@ -77,6 +77,8 @@ from .caching_descriptor_system import CachingDescriptorSystem
from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.split_mongo import encode_key_for_mongo, decode_key_from_mongo
import types
from _collections import defaultdict
log = logging.getLogger(__name__)
......@@ -1006,7 +1008,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
raise DuplicateCourseError(locator, index)
partitioned_fields = self.partition_fields_by_scope(root_category, fields)
block_fields = partitioned_fields.setdefault(Scope.settings, {})
block_fields = partitioned_fields[Scope.settings]
if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children])
definition_fields = self._serialize_fields(root_category, partitioned_fields.get(Scope.content, {}))
......@@ -1106,14 +1108,15 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
original_structure = self._lookup_course(descriptor.location)['structure']
index_entry = self._get_index_if_valid(descriptor.location, force)
definition_fields = descriptor.get_explicitly_set_fields_by_scope(Scope.content)
partitioned_fields = self.partition_xblock_fields_by_scope(descriptor)
definition_fields = partitioned_fields[Scope.content]
descriptor.definition_locator, is_updated = self.update_definition_from_data(
descriptor.definition_locator, definition_fields, user_id
)
original_entry = self._get_block_from_structure(original_structure, descriptor.location.block_id)
# check metadata
settings = descriptor.get_explicitly_set_fields_by_scope(Scope.settings)
settings = partitioned_fields[Scope.settings]
settings = self._serialize_fields(descriptor.category, settings)
if not is_updated:
is_updated = self._compare_settings(settings, original_entry['fields'])
......@@ -1235,7 +1238,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
def _persist_subdag(self, xblock, user_id, structure_blocks, new_id):
# persist the definition if persisted != passed
new_def_data = self._serialize_fields(xblock.category, xblock.get_explicitly_set_fields_by_scope(Scope.content))
partitioned_fields = self.partition_xblock_fields_by_scope(xblock)
new_def_data = self._serialize_fields(xblock.category, partitioned_fields[Scope.content])
is_updated = False
if xblock.definition_locator is None or isinstance(xblock.definition_locator.definition_id, LocalId):
xblock.definition_locator = self.create_definition_from_data(
......@@ -1270,7 +1274,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
children.append(child.block_id)
is_updated = is_updated or structure_blocks[encoded_block_id]['fields']['children'] != children
block_fields = xblock.get_explicitly_set_fields_by_scope(Scope.settings)
block_fields = partitioned_fields[Scope.settings]
block_fields = self._serialize_fields(xblock.category, block_fields)
if not is_new and not is_updated:
is_updated = self._compare_settings(block_fields, structure_blocks[encoded_block_id]['fields'])
......@@ -1697,6 +1701,18 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
index_entry['versions'][branch] = new_id
self.db_connection.update_course_index(index_entry)
def partition_xblock_fields_by_scope(self, xblock):
"""
Return a dictionary of scopes mapped to this xblock's explicitly set fields w/o any conversions
"""
# explicitly_set_fields_by_scope converts to json; so, avoiding it
# the existing partition_fields_by_scope works on a dict not an xblock
result = defaultdict(dict)
for field in xblock.fields.itervalues():
if field.is_set_on(xblock):
result[field.scope][field.name] = field.read_from(xblock)
return result
def _serialize_fields(self, category, fields):
"""
Convert any references to their serialized form.
......@@ -1708,6 +1724,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
assert isinstance(fields, dict)
xblock_class = XBlock.load_class(category, self.default_class)
xblock_class = self.mixologist.mix(xblock_class)
for field_name, value in fields.iteritems():
if value:
if isinstance(xblock_class.fields[field_name], Reference):
......@@ -1719,6 +1736,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
elif isinstance(xblock_class.fields[field_name], ReferenceValueDict):
for key, subvalue in value.iteritems():
value[key] = subvalue.block_id
# should this recurse down dicts and lists just in case they contain datetime?
elif not isinstance(value, datetime.datetime): # don't convert datetimes!
fields[field_name] = xblock_class.fields[field_name].to_json(value)
# I think these are obsolete conditions; so, I want to confirm that. Thus the warnings
if 'location' in fields:
......
# encoding: utf-8
"""
Modulestore configuration for test cases.
"""
......@@ -8,6 +9,10 @@ from django.contrib.auth.models import User
from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore import ModuleStoreEnum
import datetime
import pytz
from xmodule.tabs import CoursewareTab, CourseInfoTab, StaticTab, DiscussionTab, ProgressTab, WikiTab
from xmodule.modulestore.tests.sample_courses import default_block_info_tree, TOY_BLOCK_INFO_TREE
def mixed_store_config(data_dir, mappings):
......@@ -28,6 +33,7 @@ def mixed_store_config(data_dir, mappings):
"""
draft_mongo_config = draft_mongo_store_config(data_dir)
xml_config = xml_store_config(data_dir)
split_mongo = split_mongo_store_config(data_dir)
store = {
'default': {
......@@ -36,7 +42,8 @@ def mixed_store_config(data_dir, mappings):
'mappings': mappings,
'stores': [
draft_mongo_config['default'],
xml_config['default']
split_mongo['default'],
xml_config['default'],
]
}
}
......@@ -71,6 +78,32 @@ def draft_mongo_store_config(data_dir):
return store
def split_mongo_store_config(data_dir):
"""
Defines split module store.
"""
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': data_dir,
'render_template': 'edxmako.shortcuts.render_to_string',
}
store = {
'default': {
'NAME': 'draft',
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': {
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore{0}'.format(uuid4().hex[:5]),
},
'OPTIONS': modulestore_options
}
}
return store
def xml_store_config(data_dir):
"""
Defines default module store using XMLModuleStore.
......@@ -89,6 +122,8 @@ def xml_store_config(data_dir):
return store
class ModuleStoreTestCase(TestCase):
"""
Subclass for any test case that uses a ModuleStore.
......@@ -206,23 +241,6 @@ class ModuleStoreTestCase(TestCase):
clear_existing_modulestores()
TestCase.setUpClass()
@classmethod
def tearDownClass(cls):
"""
Drop the existing modulestores, causing them to be reloaded.
Clean up any data stored in Mongo.
"""
# Clean up by flushing the Mongo modulestore
cls.drop_mongo_collections()
# Clear out the existing modulestores,
# which will cause them to be re-created
# the next time they are accessed.
# We do this at *both* setup and teardown just to be safe.
clear_existing_modulestores()
TestCase.tearDownClass()
def _pre_setup(self):
"""
Flush the ModuleStore.
......@@ -239,6 +257,113 @@ class ModuleStoreTestCase(TestCase):
Flush the ModuleStore after each test.
"""
self.drop_mongo_collections()
# Clear out the existing modulestores,
# which will cause them to be re-created
# the next time they are accessed.
# We do this at *both* setup and teardown just to be safe.
clear_existing_modulestores()
# Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown()
def create_sample_course(self, org, course, run, block_info_tree=default_block_info_tree, course_fields=None):
"""
create a course in the default modulestore from the collection of BlockInfo
records defining the course tree
Returns:
course_loc: the CourseKey for the created course
"""
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, None):
# with self.store.bulk_write_operations(self.store.make_course_key(org, course, run)):
course = self.store.create_course(org, course, run, self.user.id, fields=course_fields)
self.course_loc = course.location
def create_sub_tree(parent_loc, block_info):
block = self.store.create_child(
self.user.id,
# TODO remove version_agnostic() when we impl the single transaction
parent_loc.version_agnostic(),
block_info.category, block_id=block_info.block_id,
fields=block_info.fields,
)
for tree in block_info.sub_tree:
create_sub_tree(block.location, tree)
setattr(self, block_info.block_id, block.location.version_agnostic())
for tree in block_info_tree:
create_sub_tree(self.course_loc, tree)
# remove version_agnostic when bulk write works
self.store.publish(self.course_loc.version_agnostic(), self.user.id)
return self.course_loc.course_key.version_agnostic()
def create_toy_course(self, org='edX', course='toy', run='2012_Fall'):
"""
Create an equivalent to the toy xml course
"""
# with self.store.bulk_write_operations(self.store.make_course_key(org, course, run)):
self.toy_loc = self.create_sample_course(
org, course, run, TOY_BLOCK_INFO_TREE,
{
"textbooks" : [["Textbook", "https://s3.amazonaws.com/edx-textbooks/guttag_computation_v3/"]],
"wiki_slug" : "toy",
"display_name" : "Toy Course",
"graded" : True,
"tabs" : [
CoursewareTab(),
CourseInfoTab(),
StaticTab(name="Syllabus", url_slug="syllabus"),
StaticTab(name="Resources", url_slug="resources"),
DiscussionTab(),
WikiTab(),
ProgressTab(),
],
"discussion_topics" : {"General" : {"id" : "i4x-edX-toy-course-2012_Fall"}},
"graceperiod" : datetime.timedelta(days=2, seconds=21599),
"start" : datetime.datetime(2015, 07, 17, 12, tzinfo=pytz.utc),
"xml_attributes" : {"filename" : ["course/2012_Fall.xml", "course/2012_Fall.xml"]},
"pdf_textbooks" : [
{
"tab_title" : "Sample Multi Chapter Textbook",
"id" : "MyTextbook",
"chapters" : [
{"url" : "/static/Chapter1.pdf", "title" : "Chapter 1"},
{"url" : "/static/Chapter2.pdf", "title" : "Chapter 2"}
]
}
],
"course_image" : "just_a_test.jpg",
}
)
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.toy_loc):
self.store.create_item(
self.user.id, self.toy_loc, "about", block_id="short_description",
fields={"data" : "A course about toys."}
)
self.store.create_item(
self.user.id, self.toy_loc, "about", block_id="effort",
fields={"data": "6 hours"}
)
self.store.create_item(
self.user.id, self.toy_loc, "about", block_id="end_date",
fields={"data": "TBD"}
)
self.store.create_item(
self.user.id, self.toy_loc, "about", block_id="overview",
fields={
"data": "<section class=\"about\">\n <h2>About This Course</h2>\n <p>Include your long course description here. The long course description should contain 150-400 words.</p>\n\n <p>This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.</p>\n</section>\n\n<section class=\"prerequisites\">\n <h2>Prerequisites</h2>\n <p>Add information about course prerequisites here.</p>\n</section>\n\n<section class=\"course-staff\">\n <h2>Course Staff</h2>\n <article class=\"teacher\">\n <div class=\"teacher-image\">\n <img src=\"/static/images/pl-faculty.png\" align=\"left\" style=\"margin:0 20 px 0\" alt=\"Course Staff Image #1\">\n </div>\n\n <h3>Staff Member #1</h3>\n <p>Biography of instructor/staff member #1</p>\n </article>\n\n <article class=\"teacher\">\n <div class=\"teacher-image\">\n <img src=\"/static/images/pl-faculty.png\" align=\"left\" style=\"margin:0 20 px 0\" alt=\"Course Staff Image #2\">\n </div>\n\n <h3>Staff Member #2</h3>\n <p>Biography of instructor/staff member #2</p>\n </article>\n</section>\n\n<section class=\"faq\">\n <section class=\"responses\">\n <h2>Frequently Asked Questions</h2>\n <article class=\"response\">\n <h3>Do I need to buy a textbook?</h3>\n <p>No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like.</p>\n </article>\n\n <article class=\"response\">\n <h3>Question #2</h3>\n <p>Your answer would be displayed here.</p>\n </article>\n </section>\n</section>\n"
}
)
self.store.create_item(
self.user.id, self.toy_loc, "course_info", "handouts",
fields={"data": "<a href='/static/handouts/sample_handout.txt'>Sample</a>"}
)
self.store.create_item(
self.user.id, self.toy_loc, "static_tab", "resources",
fields={"display_name": "Resources"},
)
self.store.create_item(
self.user.id, self.toy_loc, "static_tab", "syllabus",
fields={"display_name": "Syllabus"},
)
return self.toy_loc
......@@ -9,7 +9,7 @@ from xblock.core import XBlock
from xmodule.tabs import StaticTab
from decorator import contextmanager
from mock import Mock, patch
from nose.tools import assert_less_equal, assert_greater_equal
from nose.tools import assert_less_equal, assert_greater_equal, assert_equal
class Dummy(object):
......@@ -259,18 +259,49 @@ def check_mongo_calls(mongo_store, num_finds=0, num_sends=None):
of calls to send_message which is for insert, update, and remove (if you provide num_sends). At the
end of the with statement, it compares the counts to the num_finds and num_sends.
:param mongo_store: the MongoModulestore or subclass to watch
:param mongo_store: the MongoModulestore or subclass to watch or a SplitMongoModuleStore
:param num_finds: the exact number of find calls expected
:param num_sends: If none, don't instrument the send calls. If non-none, count and compare to
the given int value.
"""
with check_exact_number_of_calls(mongo_store.collection, mongo_store.collection.find, num_finds):
if num_sends:
with check_exact_number_of_calls(
mongo_store.database.connection,
mongo_store.database.connection._send_message, # pylint: disable=protected-access
num_sends,
):
if mongo_store.get_modulestore_type() == ModuleStoreEnum.Type.mongo:
with check_exact_number_of_calls(mongo_store.collection, mongo_store.collection.find, num_finds):
if num_sends is not None:
with check_exact_number_of_calls(
mongo_store.database.connection,
mongo_store.database.connection._send_message, # pylint: disable=protected-access
num_sends,
):
yield
else:
yield
else:
yield
elif mongo_store.get_modulestore_type() == ModuleStoreEnum.Type.split:
collections = [
mongo_store.db_connection.course_index,
mongo_store.db_connection.structures,
mongo_store.db_connection.definitions,
]
# could add else clause which raises exception or just rely on the below to suss that out
try:
find_wraps = []
wrap_patches = []
for collection in collections:
find_wrap = Mock(wraps=collection.find)
find_wraps.append(find_wrap)
wrap_patch = patch.object(collection, 'find', find_wrap)
wrap_patches.append(wrap_patch)
wrap_patch.start()
if num_sends is not None:
connection = mongo_store.db_connection.database.connection
with check_exact_number_of_calls(
connection,
connection._send_message, # pylint: disable=protected-access
num_sends,
):
yield
else:
yield
finally:
map(lambda wrap_patch: wrap_patch.stop(), wrap_patches)
call_count = sum([find_wrap.call_count for find_wrap in find_wraps])
assert_equal(call_count, num_finds)
# encoding: utf-8
"""
The data type and use of it for declaratively creating test courses.
"""
# used to create course subtrees in ModuleStoreTestCase.create_test_course
# adds to self properties w/ the given block_id which hold the UsageKey for easy retrieval.
# fields is a dictionary of keys and values. sub_tree is a collection of BlockInfo
from collections import namedtuple
import datetime
BlockInfo = namedtuple('BlockInfo', 'block_id, category, fields, sub_tree')
default_block_info_tree = [
BlockInfo(
'chapter_x', 'chapter', {}, [
BlockInfo(
'sequential_x1', 'sequential', {}, [
BlockInfo(
'vertical_x1a', 'vertical', {}, [
BlockInfo('problem_x1a_1', 'problem', {}, []),
BlockInfo('problem_x1a_2', 'problem', {}, []),
BlockInfo('problem_x1a_3', 'problem', {}, []),
BlockInfo('html_x1a_1', 'html', {}, []),
]
)
]
)
]
),
BlockInfo(
'chapter_y', 'chapter', {}, [
BlockInfo(
'sequential_y1', 'sequential', {}, [
BlockInfo(
'vertical_y1a', 'vertical', {}, [
BlockInfo('problem_y1a_1', 'problem', {}, []),
BlockInfo('problem_y1a_2', 'problem', {}, []),
BlockInfo('problem_y1a_3', 'problem', {}, []),
]
)
]
)
]
)
]
# equivalent to toy course in xml
TOY_BLOCK_INFO_TREE = [
BlockInfo(
'Overview', "chapter", {"display_name" : "Overview"}, [
BlockInfo(
"Toy_Videos", "videosequence", {
"xml_attributes": {"filename": ["", None]}, "display_name": "Toy Videos", "format": "Lecture Sequence"
}, [
BlockInfo(
"secret:toylab", "html", {
"data": "<b>Lab 2A: Superposition Experiment</b>\n\n<<<<<<< Updated upstream\n<p>Isn't the toy course great?</p>\n\n<p>Let's add some markup that uses non-ascii characters.\nFor example, we should be able to write words like encyclop&aelig;dia, or foreign words like fran&ccedil;ais.\nLooking beyond latin-1, we should handle math symbols: &pi;r&sup2 &le; &#8734.\nAnd it shouldn't matter if we use entities or numeric codes &mdash; &Omega; &ne; &pi; &equiv; &#937; &#8800; &#960;.\n</p>\n=======\n<p>Isn't the toy course great? — &le;</p>\n>>>>>>> Stashed changes\n",
"xml_attributes": { "filename" : [ "html/secret/toylab.xml", "html/secret/toylab.xml" ] },
"display_name" : "Toy lab"
}, []
),
BlockInfo(
"toyjumpto", "html", {
"data" : "<a href=\"/jump_to_id/vertical_test\">This is a link to another page and some Chinese 四節比分和七年前</a> <p>Some more Chinese 四節比分和七年前</p>\n",
"xml_attributes": { "filename" : [ "html/toyjumpto.xml", "html/toyjumpto.xml" ] }
}, []),
BlockInfo(
"toyhtml", "html", {
"data" : "<a href='/static/handouts/sample_handout.txt'>Sample</a>",
"xml_attributes" : { "filename" : [ "html/toyhtml.xml", "html/toyhtml.xml" ] }
}, []),
BlockInfo(
"nonportable", "html", {
"data": "<a href=\"/static/foo.jpg\">link</a>\n",
"xml_attributes" : { "filename" : [ "html/nonportable.xml", "html/nonportable.xml" ] }
}, []),
BlockInfo(
"nonportable_link", "html", {
"data": "<a href=\"/jump_to_id/nonportable_link\">link</a>\n\n",
"xml_attributes": {"filename": ["html/nonportable_link.xml", "html/nonportable_link.xml"]}
}, []),
BlockInfo(
"badlink", "html", {
"data": "<img src=\"/static//file.jpg\" />\n",
"xml_attributes" : { "filename" : [ "html/badlink.xml", "html/badlink.xml" ] }
}, []),
BlockInfo(
"with_styling", "html", {
"data": "<p style=\"font:italic bold 72px/30px Georgia, serif; color: red; \">Red text here</p>",
"xml_attributes": {"filename": ["html/with_styling.xml", "html/with_styling.xml"]}
}, []),
BlockInfo(
"just_img", "html", {
"data": "<img src=\"/static/foo_bar.jpg\" />",
"xml_attributes": {"filename": [ "html/just_img.xml", "html/just_img.xml" ] }
}, []),
BlockInfo(
"Video_Resources", "video", {
"youtube_id_1_0" : "1bK-WdDi6Qw", "display_name" : "Video Resources"
}, []),
]),
BlockInfo(
"Welcome", "video", {"data": "", "youtube_id_1_0": "p2Q6BrNhdh8", "display_name": "Welcome"}, []
),
BlockInfo(
"video_123456789012", "video", {"data": "", "youtube_id_1_0": "p2Q6BrNhdh8", "display_name": "Test Video"}, []
),
BlockInfo(
"video_4f66f493ac8f", "video", {"youtube_id_1_0": "p2Q6BrNhdh8"}, []
)
]
),
BlockInfo(
"secret:magic", "chapter", {
"xml_attributes": {"filename": [ "chapter/secret/magic.xml", "chapter/secret/magic.xml"]}
}, [
BlockInfo(
"toyvideo", "video", {"youtube_id_1_0": "OEoXaMPEzfMA", "display_name": "toyvideo"}, []
)
]
),
BlockInfo(
"poll_test", "chapter", {}, [
BlockInfo(
"T1_changemind_poll_foo", "poll_question", {
"question": "<p>Have you changed your mind? ’</p>",
"answers": [{"text": "Yes", "id": "yes"}, {"text": "No", "id": "no"}],
"xml_attributes": {"reset": "false", "filename": ["", None]},
"display_name": "Change your answer"
}, []) ]
),
BlockInfo(
"vertical_container", "chapter", {
"xml_attributes": {"filename": ["chapter/vertical_container.xml", "chapter/vertical_container.xml"]}
}, [
BlockInfo("vertical_sequential", "sequential", {}, [
BlockInfo("vertical_test", "vertical", {
"xml_attributes": {"filename": ["vertical/vertical_test.xml", "vertical_test"]}
}, [
BlockInfo(
"sample_video", "video", {
"youtube_id_1_25": "AKqURZnYqpk",
"youtube_id_0_75": "JMD_ifUUfsU",
"youtube_id_1_0": "OEoXaMPEzfM",
"display_name": "default",
"youtube_id_1_5": "DYpADpL7jAY"
}, []),
BlockInfo(
"separate_file_video", "video", {
"youtube_id_1_25": "AKqURZnYqpk",
"youtube_id_0_75": "JMD_ifUUfsU",
"youtube_id_1_0": "OEoXaMPEzfM",
"display_name": "default",
"youtube_id_1_5": "DYpADpL7jAY"
}, []),
BlockInfo(
"video_with_end_time", "video", {
"youtube_id_1_25": "AKqURZnYqpk",
"display_name": "default",
"youtube_id_1_0": "OEoXaMPEzfM",
"end_time": datetime.timedelta(seconds=10),
"youtube_id_1_5": "DYpADpL7jAY",
"youtube_id_0_75": "JMD_ifUUfsU"
}, []),
BlockInfo(
"T1_changemind_poll_foo_2", "poll_question", {
"question": "<p>Have you changed your mind?</p>",
"answers": [{"text": "Yes", "id": "yes"}, {"text": "No", "id": "no"}],
"xml_attributes": {"reset": "false", "filename": [ "", None]},
"display_name": "Change your answer"
}, []),
]),
BlockInfo("unicode", "html", {
"data": "…", "xml_attributes": {"filename": ["", None]}
}, [])
]),
]
),
BlockInfo(
"handout_container", "chapter", {
"xml_attributes" : {"filename" : ["chapter/handout_container.xml", "chapter/handout_container.xml"]}
}, [
BlockInfo(
"html_7e5578f25f79", "html", {
"data": "<a href=\"/static/handouts/sample_handout.txt\"> handouts</a>",
"xml_attributes": {"filename": ["", None]}
}, []
),
]
)
]
......@@ -19,6 +19,8 @@ from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
# before importing the module
# TODO remove this import and the configuration -- xmodule should not depend on django!
from django.conf import settings
from xmodule.modulestore.tests.factories import check_mongo_calls
from xmodule.modulestore.search import path_to_location
if not settings.configured:
settings.configure()
from xmodule.modulestore.mixed import MixedModuleStore
......@@ -246,49 +248,76 @@ class TestMixedModuleStore(unittest.TestCase):
SlashSeparatedCourseKey('foo', 'bar', '2012_Fall')), mongo_ms_type
)
@ddt.data('draft', 'split')
def test_has_item(self, default_ms):
# split has one lookup for the course and then one for the course items
@ddt.data(('draft', 1, 0), ('split', 2, 0))
@ddt.unpack
def test_has_item(self, default_ms, max_find, max_send):
self.initdb(default_ms)
for course_locn in self.course_locations.itervalues(): # pylint: disable=maybe-no-member
self.assertTrue(self.store.has_item(course_locn))
self._create_block_hierarchy()
self.assertTrue(self.store.has_item(self.course_locations[self.XML_COURSEID1]))
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
self.assertTrue(self.store.has_item(self.problem_x1a_1))
# try negative cases
self.assertFalse(self.store.has_item(
self.course_locations[self.XML_COURSEID1].replace(name='not_findable', category='problem')
))
self.assertFalse(self.store.has_item(self.fake_location))
with check_mongo_calls(mongo_store, max_find, max_send):
self.assertFalse(self.store.has_item(self.fake_location))
# verify that an error is raised when the revision is not valid
with self.assertRaises(UnsupportedRevisionError):
self.store.has_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
@ddt.data('draft', 'split')
def test_get_item(self, default_ms):
# draft is 2 to compute inheritance
# split is 2 (would be 3 on course b/c it looks up the wiki_slug in definitions)
@ddt.data(('draft', 2, 0), ('split', 2, 0))
@ddt.unpack
def test_get_item(self, default_ms, max_find, max_send):
self.initdb(default_ms)
for course_locn in self.course_locations.itervalues(): # pylint: disable=maybe-no-member
self.assertIsNotNone(self.store.get_item(course_locn))
self._create_block_hierarchy()
self.assertIsNotNone(self.store.get_item(self.course_locations[self.XML_COURSEID1]))
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
self.assertIsNotNone(self.store.get_item(self.problem_x1a_1))
# try negative cases
with self.assertRaises(ItemNotFoundError):
self.store.get_item(
self.course_locations[self.XML_COURSEID1].replace(name='not_findable', category='problem')
)
with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.fake_location)
with check_mongo_calls(mongo_store, max_find, max_send):
with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.fake_location)
# verify that an error is raised when the revision is not valid
with self.assertRaises(UnsupportedRevisionError):
self.store.get_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
@ddt.data('draft', 'split')
def test_get_items(self, default_ms):
# compared to get_item for the course, draft asks for both draft and published
@ddt.data(('draft', 8, 0), ('split', 2, 0))
@ddt.unpack
def test_get_items(self, default_ms, max_find, max_send):
self.initdb(default_ms)
for course_locn in self.course_locations.itervalues(): # pylint: disable=maybe-no-member
locn = course_locn.course_key
self._create_block_hierarchy()
course_locn = self.course_locations[self.XML_COURSEID1]
# NOTE: use get_course if you just want the course. get_items is expensive
modules = self.store.get_items(course_locn.course_key, category='course')
self.assertEqual(len(modules), 1)
self.assertEqual(modules[0].location, course_locn)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
course_locn = self.course_locations[self.MONGO_COURSEID]
with check_mongo_calls(mongo_store, max_find, max_send):
# NOTE: use get_course if you just want the course. get_items is expensive
modules = self.store.get_items(locn, category='course')
self.assertEqual(len(modules), 1)
self.assertEqual(modules[0].location, course_locn)
modules = self.store.get_items(course_locn.course_key, category='problem')
self.assertEqual(len(modules), 6)
# verify that an error is raised when the revision is not valid
with self.assertRaises(UnsupportedRevisionError):
......@@ -297,26 +326,35 @@ class TestMixedModuleStore(unittest.TestCase):
revision=ModuleStoreEnum.RevisionOption.draft_preferred
)
@ddt.data('draft', 'split')
def test_update_item(self, default_ms):
# draft: 2 to look in draft and then published and then 5 for updating ancestors.
# split: 3 to get the course structure & the course definition (show_calculator is scope content)
# before the change. 1 during change to refetch the definition. 3 afterward (b/c it calls get_item to return the "new" object).
# 2 sends to update index & structure (calculator is a setting field)
@ddt.data(('draft', 7, 5), ('split', 7, 2))
@ddt.unpack
def test_update_item(self, default_ms, max_find, max_send):
"""
Update should fail for r/o dbs and succeed for r/w ones
"""
self.initdb(default_ms)
self._create_block_hierarchy()
course = self.store.get_course(self.course_locations[self.XML_COURSEID1].course_key)
# if following raised, then the test is really a noop, change it
self.assertFalse(course.show_calculator, "Default changed making test meaningless")
course.show_calculator = True
with self.assertRaises(NotImplementedError): # ensure it doesn't allow writing
self.store.update_item(course, self.user_id)
# now do it for a r/w db
course = self.store.get_course(self.course_locations[self.MONGO_COURSEID].course_key)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
problem = self.store.get_item(self.problem_x1a_1)
# if following raised, then the test is really a noop, change it
self.assertFalse(course.show_calculator, "Default changed making test meaningless")
course.show_calculator = True
self.store.update_item(course, self.user_id)
course = self.store.get_course(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertTrue(course.show_calculator)
self.assertNotEqual(problem.max_attempts, 2, "Default changed making test meaningless")
problem.max_attempts = 2
with check_mongo_calls(mongo_store, max_find, max_send):
problem = self.store.update_item(problem, self.user_id)
self.assertEqual(problem.max_attempts, 2, "Update didn't persist")
@ddt.data('draft', 'split')
def test_has_changes_direct_only(self, default_ms):
......@@ -374,20 +412,33 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.publish(component.location, self.user_id)
self.assertFalse(self.store.has_changes(component.location))
@ddt.data('draft', 'split')
def test_delete_item(self, default_ms):
@ddt.data(('draft', 7, 2), ('split', 13, 4))
@ddt.unpack
def test_delete_item(self, default_ms, max_find, max_send):
"""
Delete should reject on r/o db and work on r/w one
"""
self.initdb(default_ms)
# r/o try deleting the chapter (is here to ensure it can't be deleted)
with self.assertRaises(NotImplementedError):
self.store.delete_item(self.xml_chapter_location, self.user_id)
self.store.delete_item(self.writable_chapter_location, self.user_id)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
self.store.delete_item(self.writable_chapter_location, self.user_id)
# verify it's gone
with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.writable_chapter_location)
@ddt.data(('draft', 8, 2), ('split', 13, 4))
@ddt.unpack
def test_delete_private_vertical(self, default_ms, max_find, max_send):
"""
Because old mongo treated verticals as the first layer which could be draft, it has some interesting
behavioral properties which this deletion test gets at.
"""
self.initdb(default_ms)
# create and delete a private vertical with private children
private_vert = self.store.create_child(
# don't use course_location as it may not be the repr
......@@ -411,9 +462,10 @@ class TestMixedModuleStore(unittest.TestCase):
course = self.store.get_course(self.course_locations[self.MONGO_COURSEID].course_key, 0)
self.assertIn(vert_loc, course.children)
# update the component to force it to draft w/o forcing the unit to draft
# delete the vertical and ensure the course no longer points to it
self.store.delete_item(vert_loc, self.user_id)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
self.store.delete_item(vert_loc, self.user_id)
course = self.store.get_course(self.course_locations[self.MONGO_COURSEID].course_key, 0)
if hasattr(private_vert.location, 'version_guid'):
# change to the HEAD version
......@@ -426,6 +478,14 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertFalse(self.store.has_item(leaf_loc))
self.assertNotIn(vert_loc, course.children)
@ddt.data(('draft', 4, 1), ('split', 5, 2))
@ddt.unpack
def test_delete_draft_vertical(self, default_ms, max_find, max_send):
"""
Test deleting a draft vertical which has a published version.
"""
self.initdb(default_ms)
# reproduce bug STUD-1965
# create and delete a private vertical with private children
private_vert = self.store.create_child(
......@@ -447,14 +507,19 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.publish(private_vert.location.version_agnostic(), self.user_id)
private_leaf.display_name = 'change me'
private_leaf = self.store.update_item(private_leaf, self.user_id)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
# test succeeds if delete succeeds w/o error
self.store.delete_item(private_leaf.location, self.user_id)
with check_mongo_calls(mongo_store, max_find, max_send):
self.store.delete_item(private_leaf.location, self.user_id)
@ddt.data('draft', 'split')
def test_get_courses(self, default_ms):
@ddt.data(('draft', 3, 0), ('split', 6, 0))
@ddt.unpack
def test_get_courses(self, default_ms, max_find, max_send):
self.initdb(default_ms)
# we should have 3 total courses across all stores
courses = self.store.get_courses()
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
courses = self.store.get_courses()
course_ids = [
course.location.version_agnostic()
if hasattr(course.location, 'version_agnostic') else course.location
......@@ -489,20 +554,39 @@ class TestMixedModuleStore(unittest.TestCase):
with self.assertRaises(AttributeError):
xml_store.create_course("org", "course", "run", self.user_id)
@ddt.data('draft', 'split')
def test_get_course(self, default_ms):
# draft is 2 to compute inheritance
# split is 3 b/c it gets the definition to check whether wiki is set
@ddt.data(('draft', 2, 0), ('split', 3, 0))
@ddt.unpack
def test_get_course(self, default_ms, max_find, max_send):
"""
This test is here for the performance comparison not functionality. It tests the performance
of getting an item whose scope.content fields are looked at.
"""
self.initdb(default_ms)
for course_location in self.course_locations.itervalues(): # pylint: disable=maybe-no-member
# NOTE: use get_course if you just want the course. get_items is expensive
course = self.store.get_course(course_location.course_key)
self.assertIsNotNone(course)
self.assertEqual(course.id, course_location.course_key)
@ddt.data('draft', 'split')
def test_get_parent_locations(self, default_ms):
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
course = self.store.get_item(self.course_locations[self.MONGO_COURSEID])
self.assertEqual(course.id, self.course_locations[self.MONGO_COURSEID].course_key)
course = self.store.get_item(self.course_locations[self.XML_COURSEID1])
self.assertEqual(course.id, self.course_locations[self.XML_COURSEID1].course_key)
# notice this doesn't test getting a public item via draft_preferred which draft would have 2 hits (split
# still only 2)
@ddt.data(('draft', 1, 0), ('split', 2, 0))
@ddt.unpack
def test_get_parent_locations(self, default_ms, max_find, max_send):
"""
Test a simple get parent for a direct only category (i.e, always published)
"""
self.initdb(default_ms)
parent = self.store.get_parent_location(self.writable_chapter_location)
self.assertEqual(parent, self.course_locations[self.MONGO_COURSEID])
self._create_block_hierarchy()
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
parent = self.store.get_parent_location(self.problem_x1a_1)
self.assertEqual(parent, self.vertical_x1a)
parent = self.store.get_parent_location(self.xml_chapter_location)
self.assertEqual(parent, self.course_locations[self.XML_COURSEID1])
......@@ -584,6 +668,62 @@ class TestMixedModuleStore(unittest.TestCase):
(child_to_delete_location, None, ModuleStoreEnum.RevisionOption.published_only),
])
@ddt.data(('draft', [10, 3], 0), ('split', [14, 6], 0))
@ddt.unpack
def test_path_to_location(self, default_ms, num_finds, num_sends):
"""
Make sure that path_to_location works
"""
self.initdb(default_ms)
self._create_block_hierarchy()
course_key = self.course_locations[self.MONGO_COURSEID].course_key
should_work = (
(self.problem_x1a_2,
(course_key, u"Chapter_x", u"Sequential_x1", '1')),
(self.chapter_x,
(course_key, "Chapter_x", None, None)),
)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
for location, expected in should_work:
with check_mongo_calls(mongo_store, num_finds.pop(0), num_sends):
self.assertEqual(path_to_location(self.store, location), expected)
not_found = (
course_key.make_usage_key('video', 'WelcomeX'),
course_key.make_usage_key('course', 'NotHome'),
)
for location in not_found:
with self.assertRaises(ItemNotFoundError):
path_to_location(self.store, location)
def test_xml_path_to_location(self):
"""
Make sure that path_to_location works: should be passed a modulestore
with the toy and simple courses loaded.
"""
# only needs course_locations set
self.initdb('draft')
course_key = self.course_locations[self.XML_COURSEID1].course_key
should_work = (
(course_key.make_usage_key('video', 'Welcome'),
(course_key, "Overview", "Welcome", None)),
(course_key.make_usage_key('chapter', 'Overview'),
(course_key, "Overview", None, None)),
)
for location, expected in should_work:
self.assertEqual(path_to_location(self.store, location), expected)
not_found = (
course_key.make_usage_key('video', 'WelcomeX'),
course_key.make_usage_key('course', 'NotHome'),
)
for location in not_found:
with self.assertRaises(ItemNotFoundError):
path_to_location(self.store, location)
@ddt.data('draft')
def test_revert_to_published_root_draft(self, default_ms):
"""
......@@ -669,8 +809,12 @@ class TestMixedModuleStore(unittest.TestCase):
# It does not discard the child vertical, even though that child is a draft (with no published version)
self.assertEqual(1, len(reverted_parent.children))
@ddt.data('draft', 'split')
def test_get_orphans(self, default_ms):
@ddt.data(('draft', 1, 0), ('split', 2, 0))
@ddt.unpack
def test_get_orphans(self, default_ms, max_find, max_send):
"""
Test finding orphans.
"""
self.initdb(default_ms)
course_id = self.course_locations[self.MONGO_COURSEID].course_key
......@@ -700,7 +844,9 @@ class TestMixedModuleStore(unittest.TestCase):
block_id=location.block_id
)
found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertEqual(set(found_orphans), set(orphan_locations))
@ddt.data('draft')
......@@ -741,8 +887,9 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(self.user_id, block.subtree_edited_by)
self.assertGreater(datetime.datetime.now(UTC), block.subtree_edited_on)
@ddt.data('draft', 'split')
def test_get_courses_for_wiki(self, default_ms):
@ddt.data(('draft', 1, 0), ('split', 1, 0))
@ddt.unpack
def test_get_courses_for_wiki(self, default_ms, max_find, max_send):
"""
Test the get_courses_for_wiki method
"""
......@@ -757,7 +904,9 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertIn(self.course_locations[self.XML_COURSEID2].course_key, wiki_courses)
# Test Mongo wiki
wiki_courses = self.store.get_courses_for_wiki('999')
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
wiki_courses = self.store.get_courses_for_wiki('999')
self.assertEqual(len(wiki_courses), 1)
self.assertIn(
self.course_locations[self.MONGO_COURSEID].course_key.replace(branch=None), # Branch agnostic
......@@ -767,8 +916,9 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(len(self.store.get_courses_for_wiki('edX.simple.2012_Fall')), 0)
self.assertEqual(len(self.store.get_courses_for_wiki('no_such_wiki')), 0)
@ddt.data('draft', 'split')
def test_unpublish(self, default_ms):
@ddt.data(('draft', 2, 6), ('split', 7, 2))
@ddt.unpack
def test_unpublish(self, default_ms, max_find, max_send):
"""
Test calling unpublish
"""
......@@ -784,7 +934,9 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertIsNotNone(published_xblock)
# unpublish
self.store.unpublish(self.vertical_x1a, self.user_id)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
self.store.unpublish(self.vertical_x1a, self.user_id)
with self.assertRaises(ItemNotFoundError):
self.store.get_item(
......@@ -799,8 +951,9 @@ class TestMixedModuleStore(unittest.TestCase):
)
self.assertIsNotNone(draft_xblock)
@ddt.data('draft', 'split')
def test_compute_publish_state(self, default_ms):
@ddt.data(('draft', 1, 0), ('split', 4, 0))
@ddt.unpack
def test_compute_publish_state(self, default_ms, max_find, max_send):
"""
Test the compute_publish_state method
"""
......@@ -810,7 +963,9 @@ class TestMixedModuleStore(unittest.TestCase):
# start off as Private
item = self.store.create_child(self.user_id, self.writable_chapter_location, 'problem', 'test_compute_publish_state')
item_location = item.location.version_agnostic()
self.assertEquals(self.store.compute_publish_state(item), PublishState.private)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send):
self.assertEquals(self.store.compute_publish_state(item), PublishState.private)
# Private -> Public
self.store.publish(item_location, self.user_id)
......
from nose.tools import assert_equals, assert_raises, assert_true, assert_false # pylint: disable=E0611
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.search import path_to_location
from opaque_keys.edx.locations import SlashSeparatedCourseKey
def check_path_to_location(modulestore):
"""
Make sure that path_to_location works: should be passed a modulestore
with the toy and simple courses loaded.
"""
course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
should_work = (
(course_id.make_usage_key('video', 'Welcome'),
(course_id, "Overview", "Welcome", None)),
(course_id.make_usage_key('chapter', 'Overview'),
(course_id, "Overview", None, None)),
)
for location, expected in should_work:
assert_equals(path_to_location(modulestore, location), expected)
not_found = (
course_id.make_usage_key('video', 'WelcomeX'),
course_id.make_usage_key('course', 'NotHome'),
)
for location in not_found:
with assert_raises(ItemNotFoundError):
path_to_location(modulestore, location)
from nose.tools import assert_equals, assert_true, assert_false # pylint: disable=E0611
def check_has_course_method(modulestore, locator, locator_key_fields):
......
......@@ -31,13 +31,11 @@ from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
from xmodule.contentstore.mongo import MongoContentStore
from xmodule.modulestore.tests.test_modulestore import check_path_to_location
from nose.tools import assert_in
from xmodule.exceptions import NotFoundError
from git.test.lib.asserts import assert_not_none
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.mongo.base import as_draft
from xmodule.modulestore.tests.factories import check_mongo_calls
log = logging.getLogger(__name__)
......@@ -245,11 +243,6 @@ class TestMongoModuleStore(unittest.TestCase):
self.draft_store._find_one(Location('edX', 'toy', '2012_Fall', 'video', 'Welcome')),
)
def test_path_to_location(self):
'''Make sure that path_to_location works'''
with check_mongo_calls(self.draft_store, 9):
check_path_to_location(self.draft_store)
def test_xlinter(self):
'''
Run through the xlinter, we know the 'toy' course has violations, but the
......
......@@ -1464,19 +1464,11 @@ class TestCourseCreation(SplitModuleTest):
original_locator = CourseLocator(org='guestx', course='contender', run="run", branch=BRANCH_NAME_DRAFT)
original = modulestore().get_course(original_locator)
original_index = modulestore().get_course_index_info(original_locator)
fields = {}
for field in original.fields.values():
value = getattr(original, field.name)
if not isinstance(value, datetime.datetime):
json_value = field.to_json(value)
else:
json_value = value
if field.scope == Scope.content and field.name != 'location':
fields[field.name] = json_value
elif field.scope == Scope.settings:
fields[field.name] = json_value
fields = {
'grading_policy': original.grading_policy,
'display_name': 'Derivative',
}
fields['grading_policy']['GRADE_CUTOFFS'] = {'A': .9, 'B': .8, 'C': .65}
fields['display_name'] = 'Derivative'
new_draft = modulestore().create_course(
'counter', 'leech', 'leech_run', 'leech_master', BRANCH_NAME_DRAFT,
versions_dict={BRANCH_NAME_DRAFT: original_index['versions'][BRANCH_NAME_DRAFT]},
......
......@@ -8,10 +8,8 @@ from glob import glob
from mock import patch
from xmodule.modulestore.xml import XMLModuleStore
from opaque_keys.edx.locations import Location
from xmodule.modulestore import ModuleStoreEnum
from .test_modulestore import check_path_to_location
from xmodule.tests import DATA_DIR
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.test_modulestore import check_has_course_method
......@@ -32,15 +30,6 @@ class TestXMLModuleStore(unittest.TestCase):
"""
Test around the XML modulestore
"""
def test_path_to_location(self):
"""Make sure that path_to_location works properly"""
print "Starting import"
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'simple'])
print "finished import"
check_path_to_location(modulestore)
def test_xml_modulestore_type(self):
store = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'simple'])
self.assertEqual(store.get_modulestore_type(), ModuleStoreEnum.Type.xml)
......
"""
Test for lms courseware app, module render unit
"""
from ddt import ddt, data
import ddt
from functools import partial
from mock import MagicMock, patch, Mock
import json
......@@ -9,7 +9,6 @@ import json
from django.http import Http404, HttpResponse
from django.core.urlresolvers import reverse
from django.conf import settings
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.contrib.auth.models import AnonymousUser
......@@ -21,7 +20,7 @@ from xblock.fields import ScopeIds
from xmodule.lti_module import LTIDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory, check_mongo_calls
from xmodule.x_module import XModuleDescriptor, STUDENT_VIEW
from opaque_keys.edx.locations import SlashSeparatedCourseKey
......@@ -39,6 +38,7 @@ from courseware.tests.test_submitting_problems import TestSubmittingProblems
from student.models import anonymous_id_for_user
from lms.lib.xblock.runtime import quote_slashes
from xmodule.modulestore import ModuleStoreEnum
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......@@ -314,70 +314,75 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestTOC(TestCase):
@ddt.ddt
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestTOC(ModuleStoreTestCase):
"""Check the Table of Contents for a course"""
def setUp(self):
# Toy courses should be loaded
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.toy_course = modulestore().get_course(self.course_key)
self.portal_user = UserFactory()
def test_toc_toy_from_chapter(self):
chapter = 'Overview'
chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter)
def setup_modulestore(self, default_ms, num_finds, num_sends):
self.course_key = self.create_toy_course()
self.chapter = 'Overview'
chapter_url = '%s/%s/%s' % ('/courses', self.course_key, self.chapter)
factory = RequestFactory()
request = factory.get(chapter_url)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.toy_course.id, self.portal_user, self.toy_course, depth=2)
expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': None, 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': None, 'active': False},
{'url_name': 'video_123456789012', 'display_name': 'Test Video', 'graded': True,
'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, field_data_cache)
self.request = factory.get(chapter_url)
self.request.user = UserFactory()
self.modulestore = self.store._get_modulestore_for_courseid(self.course_key)
with check_mongo_calls(self.modulestore, num_finds, num_sends):
self.toy_course = self.store.get_course(self.toy_loc, depth=2)
self.field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.toy_loc, self.request.user, self.toy_course, depth=2)
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0), (ModuleStoreEnum.Type.split, 7, 0))
@ddt.unpack
def test_toc_toy_from_chapter(self, default_ms, num_finds, num_sends):
with self.store.default_store(default_ms):
self.setup_modulestore(default_ms, num_finds, num_sends)
expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': None, 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': None, 'active': False},
{'url_name': 'video_123456789012', 'display_name': 'Test Video', 'graded': True,
'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
with check_mongo_calls(self.modulestore, 0, 0):
actual = render.toc_for_course(
self.request.user, self.request, self.toy_course, self.chapter, None, self.field_data_cache
)
for toc_section in expected:
self.assertIn(toc_section, actual)
def test_toc_toy_from_section(self):
chapter = 'Overview'
chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter)
section = 'Welcome'
factory = RequestFactory()
request = factory.get(chapter_url)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.toy_course.id, self.portal_user, self.toy_course, depth=2)
expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': None, 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': None, 'active': True},
{'url_name': 'video_123456789012', 'display_name': 'Test Video', 'graded': True,
'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, field_data_cache)
for toc_section in expected:
self.assertIn(toc_section, actual)
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0), (ModuleStoreEnum.Type.split, 7, 0))
@ddt.unpack
def test_toc_toy_from_section(self, default_ms, num_finds, num_sends):
with self.store.default_store(default_ms):
self.setup_modulestore(default_ms, num_finds, num_sends)
section = 'Welcome'
expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': None, 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': None, 'active': True},
{'url_name': 'video_123456789012', 'display_name': 'Test Video', 'graded': True,
'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.request.user, self.request, self.toy_course, self.chapter, section, self.field_data_cache)
for toc_section in expected:
self.assertIn(toc_section, actual)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......@@ -765,7 +770,7 @@ PER_STUDENT_ANONYMIZED_DESCRIPTORS = set(
)
@ddt
@ddt.ddt
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
......@@ -805,7 +810,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
Mock(), # XQueue Callback Url Prefix
).xmodule_runtime.anonymous_student_id
@data(*PER_STUDENT_ANONYMIZED_DESCRIPTORS)
@ddt.data(*PER_STUDENT_ANONYMIZED_DESCRIPTORS)
def test_per_student_anonymized_id(self, descriptor_class):
for course_id in ('MITx/6.00x/2012_Fall', 'MITx/6.00x/2013_Spring'):
self.assertEquals(
......@@ -815,7 +820,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
self._get_anonymous_id(SlashSeparatedCourseKey.from_deprecated_string(course_id), descriptor_class)
)
@data(*PER_COURSE_ANONYMIZED_DESCRIPTORS)
@ddt.data(*PER_COURSE_ANONYMIZED_DESCRIPTORS)
def test_per_course_anonymized_id(self, descriptor_class):
self.assertEquals(
# This value is set by observation, so that later changes to the student
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment