257 lines
13 KiB
Python
257 lines
13 KiB
Python
import unittest
|
|
from xmodule import templates
|
|
from xmodule.modulestore.tests import persistent_factories
|
|
from xmodule.course_module import CourseDescriptor
|
|
from xmodule.modulestore.django import modulestore, loc_mapper, clear_existing_modulestores
|
|
from xmodule.seq_module import SequenceDescriptor
|
|
from xmodule.capa_module import CapaDescriptor
|
|
from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator
|
|
from xmodule.modulestore.exceptions import ItemNotFoundError
|
|
from xmodule.html_module import HtmlDescriptor
|
|
from xmodule.modulestore import inheritance
|
|
from xmodule.x_module import XModuleDescriptor
|
|
|
|
|
|
class TemplateTests(unittest.TestCase):
|
|
"""
|
|
Test finding and using the templates (boilerplates) for xblocks.
|
|
"""
|
|
|
|
def setUp(self):
|
|
clear_existing_modulestores()
|
|
|
|
def test_get_templates(self):
|
|
found = templates.all_templates()
|
|
self.assertIsNotNone(found.get('course'))
|
|
self.assertIsNotNone(found.get('about'))
|
|
self.assertIsNotNone(found.get('html'))
|
|
self.assertIsNotNone(found.get('problem'))
|
|
self.assertEqual(len(found.get('course')), 0)
|
|
self.assertEqual(len(found.get('about')), 1)
|
|
self.assertGreaterEqual(len(found.get('html')), 2)
|
|
self.assertGreaterEqual(len(found.get('problem')), 10)
|
|
dropdown = None
|
|
for template in found['problem']:
|
|
self.assertIn('metadata', template)
|
|
self.assertIn('display_name', template['metadata'])
|
|
if template['metadata']['display_name'] == 'Dropdown':
|
|
dropdown = template
|
|
break
|
|
self.assertIsNotNone(dropdown)
|
|
self.assertIn('markdown', dropdown['metadata'])
|
|
self.assertIn('data', dropdown)
|
|
self.assertRegexpMatches(dropdown['metadata']['markdown'], r'^Dropdown.*')
|
|
self.assertRegexpMatches(dropdown['data'], r'<problem>\s*<p>Dropdown.*')
|
|
|
|
def test_get_some_templates(self):
|
|
self.assertEqual(len(SequenceDescriptor.templates()), 0)
|
|
self.assertGreater(len(HtmlDescriptor.templates()), 0)
|
|
self.assertIsNone(SequenceDescriptor.get_template('doesntexist.yaml'))
|
|
self.assertIsNone(HtmlDescriptor.get_template('doesntexist.yaml'))
|
|
self.assertIsNotNone(HtmlDescriptor.get_template('announcement.yaml'))
|
|
|
|
def test_factories(self):
|
|
test_course = persistent_factories.PersistentCourseFactory.create(org='testx', prettyid='tempcourse',
|
|
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')
|
|
|
|
test_chapter = persistent_factories.ItemFactory.create(display_name='chapter 1',
|
|
parent_location=test_course.location)
|
|
self.assertIsInstance(test_chapter, SequenceDescriptor)
|
|
# refetch parent which should now point to child
|
|
test_course = modulestore('split').get_course(test_chapter.location)
|
|
self.assertIn(test_chapter.location.usage_id, test_course.children)
|
|
|
|
def test_temporary_xblocks(self):
|
|
"""
|
|
Test using load_from_json to create non persisted xblocks
|
|
"""
|
|
test_course = persistent_factories.PersistentCourseFactory.create(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)
|
|
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 = '<problem>boo</problem>'
|
|
test_problem = self.load_from_json({'category': 'problem',
|
|
'fields': {'data': test_def_content}},
|
|
test_course.system, 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())
|
|
test_problem.display_name = 'test problem'
|
|
self.assertEqual(test_problem.display_name, 'test problem')
|
|
|
|
def test_persist_dag(self):
|
|
"""
|
|
try saving temporary xblocks
|
|
"""
|
|
test_course = persistent_factories.PersistentCourseFactory.create(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)
|
|
test_def_content = '<problem>boo</problem>'
|
|
# create child
|
|
_ = self.load_from_json({'category': 'problem',
|
|
'fields': {'data': test_def_content}},
|
|
test_course.system, parent_xblock=test_chapter)
|
|
# better to pass in persisted parent over the subdag so
|
|
# subdag gets the parent pointer (otherwise 2 ops, persist dag, update parent children,
|
|
# persist parent
|
|
persisted_course = modulestore('split').persist_xblock_dag(test_course, 'testbot')
|
|
self.assertEqual(len(persisted_course.children), 1)
|
|
persisted_chapter = persisted_course.get_children()[0]
|
|
self.assertEqual(persisted_chapter.category, 'chapter')
|
|
self.assertEqual(persisted_chapter.display_name, 'chapter n')
|
|
self.assertEqual(len(persisted_chapter.children), 1)
|
|
persisted_problem = persisted_chapter.get_children()[0]
|
|
self.assertEqual(persisted_problem.category, 'problem')
|
|
self.assertEqual(persisted_problem.data, test_def_content)
|
|
|
|
def test_delete_course(self):
|
|
test_course = persistent_factories.PersistentCourseFactory.create(
|
|
org='testx',
|
|
prettyid='edu.harvard.history.doomed',
|
|
display_name='doomed test course',
|
|
user_id='testbot')
|
|
persistent_factories.ItemFactory.create(display_name='chapter 1',
|
|
parent_location=test_course.location)
|
|
|
|
id_locator = CourseLocator(course_id=test_course.location.course_id, branch='draft')
|
|
guid_locator = CourseLocator(version_guid=test_course.location.version_guid)
|
|
# verify it can be retireved by id
|
|
self.assertIsInstance(modulestore('split').get_course(id_locator), CourseDescriptor)
|
|
# and by guid
|
|
self.assertIsInstance(modulestore('split').get_course(guid_locator), CourseDescriptor)
|
|
modulestore('split').delete_course(id_locator.course_id)
|
|
# test can no longer retrieve by id
|
|
self.assertRaises(ItemNotFoundError, modulestore('split').get_course, id_locator)
|
|
# but can by guid
|
|
self.assertIsInstance(modulestore('split').get_course(guid_locator), CourseDescriptor)
|
|
|
|
def test_block_generations(self):
|
|
"""
|
|
Test get_block_generations
|
|
"""
|
|
test_course = persistent_factories.PersistentCourseFactory.create(
|
|
org='testx',
|
|
prettyid='edu.harvard.history.hist101',
|
|
display_name='history test course',
|
|
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',
|
|
parent_location=chapter.location, user_id='testbot', category='vertical')
|
|
first_problem = persistent_factories.ItemFactory.create(
|
|
display_name='problem 1', parent_location=sub.location, user_id='testbot', category='problem',
|
|
data="<problem></problem>"
|
|
)
|
|
first_problem.max_attempts = 3
|
|
first_problem.save() # decache the above into the kvs
|
|
updated_problem = modulestore('split').update_item(first_problem, 'testbot')
|
|
self.assertIsNotNone(updated_problem.previous_version)
|
|
self.assertEqual(updated_problem.previous_version, first_problem.update_version)
|
|
self.assertNotEqual(updated_problem.update_version, first_problem.update_version)
|
|
updated_loc = modulestore('split').delete_item(updated_problem.location, 'testbot', delete_children=True)
|
|
|
|
second_problem = persistent_factories.ItemFactory.create(
|
|
display_name='problem 2',
|
|
parent_location=BlockUsageLocator(updated_loc, usage_id=sub.location.usage_id),
|
|
user_id='testbot', category='problem',
|
|
data="<problem></problem>"
|
|
)
|
|
|
|
# course root only updated 2x
|
|
version_history = modulestore('split').get_block_generations(test_course.location)
|
|
self.assertEqual(version_history.locator.version_guid, test_course.location.version_guid)
|
|
self.assertEqual(len(version_history.children), 1)
|
|
self.assertEqual(version_history.children[0].children, [])
|
|
self.assertEqual(version_history.children[0].locator.version_guid, chapter.location.version_guid)
|
|
|
|
# sub changed on add, add problem, delete problem, add problem in strict linear seq
|
|
version_history = modulestore('split').get_block_generations(sub.location)
|
|
self.assertEqual(len(version_history.children), 1)
|
|
self.assertEqual(len(version_history.children[0].children), 1)
|
|
self.assertEqual(len(version_history.children[0].children[0].children), 1)
|
|
self.assertEqual(len(version_history.children[0].children[0].children[0].children), 0)
|
|
|
|
# first and second problem may show as same usage_id; so, need to ensure their histories are right
|
|
version_history = modulestore('split').get_block_generations(updated_problem.location)
|
|
self.assertEqual(version_history.locator.version_guid, first_problem.location.version_guid)
|
|
self.assertEqual(len(version_history.children), 1) # updated max_attempts
|
|
self.assertEqual(len(version_history.children[0].children), 0)
|
|
|
|
version_history = modulestore('split').get_block_generations(second_problem.location)
|
|
self.assertNotEqual(version_history.locator.version_guid, first_problem.location.version_guid)
|
|
|
|
def test_split_inject_loc_mapper(self):
|
|
"""
|
|
Test that creating a loc_mapper causes it to automatically attach to the split mongo store
|
|
"""
|
|
# instantiate location mapper before split
|
|
mapper = loc_mapper()
|
|
# split must inject the location mapper itself since the mapper existed before it did
|
|
self.assertEqual(modulestore('split').loc_mapper, mapper)
|
|
|
|
def test_loc_inject_into_split(self):
|
|
"""
|
|
Test that creating a loc_mapper causes it to automatically attach to the split mongo store
|
|
"""
|
|
# force instantiation of split modulestore before there's a location mapper and verify
|
|
# it has no pointer to loc mapper
|
|
self.assertIsNone(modulestore('split').loc_mapper)
|
|
# force instantiation of location mapper which must inject itself into the split
|
|
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_ = XModuleDescriptor.load_class(
|
|
json_data.get('category', json_data.get('location', {}).get('category')),
|
|
default_class
|
|
)
|
|
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
|
|
|