Commit e4a69373 by Don Mitchell

xblock fields persist w/o breaking by scope

Letting xblocks handle scope rather than separating fields into
different attrs. Although, split still shunts content fields to a
different collection than setting and children fields.

The big difference is that content fields will always be a dict and not
sometimes just a string and there's no special casing of 'data' attr.

The other mind change is no more 'metadata' dict.
parent a2dcf9aa
...@@ -75,7 +75,7 @@ class TemplateTests(unittest.TestCase): ...@@ -75,7 +75,7 @@ class TemplateTests(unittest.TestCase):
display_name='fun test course', user_id='testbot') display_name='fun test course', user_id='testbot')
test_chapter = XModuleDescriptor.load_from_json({'category': 'chapter', test_chapter = XModuleDescriptor.load_from_json({'category': 'chapter',
'metadata': {'display_name': 'chapter n'}}, 'fields': {'display_name': 'chapter n'}},
test_course.system, parent_xblock=test_course) test_course.system, parent_xblock=test_course)
self.assertIsInstance(test_chapter, SequenceDescriptor) self.assertIsInstance(test_chapter, SequenceDescriptor)
self.assertEqual(test_chapter.display_name, 'chapter n') self.assertEqual(test_chapter.display_name, 'chapter n')
...@@ -84,7 +84,7 @@ class TemplateTests(unittest.TestCase): ...@@ -84,7 +84,7 @@ class TemplateTests(unittest.TestCase):
# test w/ a definition (e.g., a problem) # test w/ a definition (e.g., a problem)
test_def_content = '<problem>boo</problem>' test_def_content = '<problem>boo</problem>'
test_problem = XModuleDescriptor.load_from_json({'category': 'problem', test_problem = XModuleDescriptor.load_from_json({'category': 'problem',
'definition': {'data': test_def_content}}, 'fields': {'data': test_def_content}},
test_course.system, parent_xblock=test_chapter) test_course.system, parent_xblock=test_chapter)
self.assertIsInstance(test_problem, CapaDescriptor) self.assertIsInstance(test_problem, CapaDescriptor)
self.assertEqual(test_problem.data, test_def_content) self.assertEqual(test_problem.data, test_def_content)
...@@ -99,11 +99,12 @@ class TemplateTests(unittest.TestCase): ...@@ -99,11 +99,12 @@ class TemplateTests(unittest.TestCase):
test_course = persistent_factories.PersistentCourseFactory.create(org='testx', prettyid='tempcourse', test_course = persistent_factories.PersistentCourseFactory.create(org='testx', prettyid='tempcourse',
display_name='fun test course', user_id='testbot') display_name='fun test course', user_id='testbot')
test_chapter = XModuleDescriptor.load_from_json({'category': 'chapter', test_chapter = XModuleDescriptor.load_from_json({'category': 'chapter',
'metadata': {'display_name': 'chapter n'}}, 'fields': {'display_name': 'chapter n'}},
test_course.system, parent_xblock=test_course) test_course.system, parent_xblock=test_course)
test_def_content = '<problem>boo</problem>' test_def_content = '<problem>boo</problem>'
test_problem = XModuleDescriptor.load_from_json({'category': 'problem', # create child
'definition': {'data': test_def_content}}, _ = XModuleDescriptor.load_from_json({'category': 'problem',
'fields': {'data': test_def_content}},
test_course.system, parent_xblock=test_chapter) test_course.system, parent_xblock=test_chapter)
# better to pass in persisted parent over the subdag so # better to pass in persisted parent over the subdag so
# subdag gets the parent pointer (otherwise 2 ops, persist dag, update parent children, # subdag gets the parent pointer (otherwise 2 ops, persist dag, update parent children,
...@@ -152,15 +153,24 @@ class TemplateTests(unittest.TestCase): ...@@ -152,15 +153,24 @@ class TemplateTests(unittest.TestCase):
parent_location=test_course.location, user_id='testbot') parent_location=test_course.location, user_id='testbot')
sub = persistent_factories.ItemFactory.create(display_name='subsection 1', sub = persistent_factories.ItemFactory.create(display_name='subsection 1',
parent_location=chapter.location, user_id='testbot', category='vertical') parent_location=chapter.location, user_id='testbot', category='vertical')
first_problem = persistent_factories.ItemFactory.create(display_name='problem 1', first_problem = persistent_factories.ItemFactory.create(
parent_location=sub.location, user_id='testbot', category='problem', data="<problem></problem>") display_name='problem 1', parent_location=sub.location, user_id='testbot', category='problem',
fields={'data':"<problem></problem>"}
)
first_problem.max_attempts = 3 first_problem.max_attempts = 3
first_problem.save() # decache the above into the kvs
updated_problem = modulestore('split').update_item(first_problem, 'testbot') updated_problem = modulestore('split').update_item(first_problem, 'testbot')
updated_loc = modulestore('split').delete_item(updated_problem.location, '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', second_problem = persistent_factories.ItemFactory.create(
display_name='problem 2',
parent_location=BlockUsageLocator(updated_loc, usage_id=sub.location.usage_id), parent_location=BlockUsageLocator(updated_loc, usage_id=sub.location.usage_id),
user_id='testbot', category='problem', data="<problem></problem>") user_id='testbot', category='problem',
fields={'data':"<problem></problem>"}
)
# course root only updated 2x # course root only updated 2x
version_history = modulestore('split').get_block_generations(test_course.location) version_history = modulestore('split').get_block_generations(test_course.location)
......
...@@ -11,18 +11,17 @@ from .split_mongo_kvs import SplitMongoKVS, SplitMongoKVSid ...@@ -11,18 +11,17 @@ from .split_mongo_kvs import SplitMongoKVS, SplitMongoKVSid
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# TODO should this be here or w/ x_module or ???
class CachingDescriptorSystem(MakoDescriptorSystem): class CachingDescriptorSystem(MakoDescriptorSystem):
""" """
A system that has a cache of a course version's json that it will use to load modules A system that has a cache of a course version's json that it will use to load modules
from, with a backup of calling to the underlying modulestore for more data. from, with a backup of calling to the underlying modulestore for more data.
Computes the metadata inheritance upon creation. Computes the settings (nee 'metadata') inheritance upon creation.
""" """
def __init__(self, modulestore, course_entry, module_data, lazy, def __init__(self, modulestore, course_entry, module_data, lazy,
default_class, error_tracker, render_template): default_class, error_tracker, render_template):
""" """
Computes the metadata inheritance and sets up the cache. Computes the settings inheritance and sets up the cache.
modulestore: the module store that can be used to retrieve additional modulestore: the module store that can be used to retrieve additional
modules modules
...@@ -50,9 +49,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -50,9 +49,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self.default_class = default_class self.default_class = default_class
# TODO see if self.course_id is needed: is already in course_entry but could be > 1 value # TODO see if self.course_id is needed: is already in course_entry but could be > 1 value
# Compute inheritance # Compute inheritance
modulestore.inherit_metadata(course_entry.get('blocks', {}), modulestore.inherit_settings(
course_entry.get('blocks', {}) course_entry.get('blocks', {}),
.get(course_entry.get('root'))) course_entry.get('blocks', {}).get(course_entry.get('root'))
)
def _load_item(self, usage_id, course_entry_override=None): def _load_item(self, usage_id, course_entry_override=None):
# TODO ensure all callers of system.load_item pass just the id # TODO ensure all callers of system.load_item pass just the id
...@@ -73,9 +73,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -73,9 +73,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None): def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None):
if course_entry_override is None: if course_entry_override is None:
course_entry_override = self.course_entry course_entry_override = self.course_entry
# most likely a lazy loader but not the id directly # most likely a lazy loader or the id directly
definition = json_data.get('definition', {}) definition = json_data.get('definition', {})
metadata = json_data.get('metadata', {})
block_locator = BlockUsageLocator( block_locator = BlockUsageLocator(
version_guid=course_entry_override['_id'], version_guid=course_entry_override['_id'],
...@@ -86,9 +85,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -86,9 +85,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
kvs = SplitMongoKVS( kvs = SplitMongoKVS(
definition, definition,
json_data.get('children', []), json_data.get('fields', {}),
metadata, json_data.get('_inherited_settings'),
json_data.get('_inherited_metadata'),
block_locator, block_locator,
json_data.get('category')) json_data.get('category'))
model_data = DbModel(kvs, class_, None, model_data = DbModel(kvs, class_, None,
...@@ -111,10 +109,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -111,10 +109,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
error_msg=exc_info_to_str(sys.exc_info()) error_msg=exc_info_to_str(sys.exc_info())
) )
module.edited_by = json_data.get('edited_by') edit_info = json_data.get('edit_info', {})
module.edited_on = json_data.get('edited_on') module.edited_by = edit_info.get('edited_by')
module.previous_version = json_data.get('previous_version') module.edited_on = edit_info.get('edited_on')
module.update_version = json_data.get('update_version') module.previous_version = edit_info.get('previous_version')
module.update_version = edit_info.get('update_version')
module.definition_locator = self.modulestore.definition_locator(definition) module.definition_locator = self.modulestore.definition_locator(definition)
# decache any pending field settings # decache any pending field settings
module.save() module.save()
......
...@@ -16,6 +16,9 @@ from .. import ModuleStoreBase ...@@ -16,6 +16,9 @@ from .. import ModuleStoreBase
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from .caching_descriptor_system import CachingDescriptorSystem from .caching_descriptor_system import CachingDescriptorSystem
from xblock.core import Scope
from pytz import UTC
import collections
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#============================================================================== #==============================================================================
...@@ -102,10 +105,12 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -102,10 +105,12 @@ class SplitMongoModuleStore(ModuleStoreBase):
''' '''
new_module_data = {} new_module_data = {}
for usage_id in base_usage_ids: for usage_id in base_usage_ids:
new_module_data = self.descendants(system.course_entry['blocks'], new_module_data = self.descendants(
system.course_entry['blocks'],
usage_id, usage_id,
depth, depth,
new_module_data) new_module_data
)
# remove any which were already in module_data (not sure if there's a better way) # remove any which were already in module_data (not sure if there's a better way)
for newkey in new_module_data.iterkeys(): for newkey in new_module_data.iterkeys():
...@@ -114,8 +119,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -114,8 +119,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
if lazy: if lazy:
for block in new_module_data.itervalues(): for block in new_module_data.itervalues():
block['definition'] = DefinitionLazyLoader(self, block['definition'] = DefinitionLazyLoader(self, block['definition'])
block['definition'])
else: else:
# Load all descendants by id # Load all descendants by id
descendent_definitions = self.definitions.find({ descendent_definitions = self.definitions.find({
...@@ -127,7 +131,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -127,7 +131,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
for block in new_module_data.itervalues(): for block in new_module_data.itervalues():
if block['definition'] in definitions: if block['definition'] in definitions:
block['definition'] = definitions[block['definition']] block['fields'].update(definitions[block['definition']].get('fields'))
system.module_data.update(new_module_data) system.module_data.update(new_module_data)
return system.module_data return system.module_data
...@@ -317,7 +321,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -317,7 +321,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
definitions. definitions.
Common qualifiers are category, definition (provide definition id), Common qualifiers are category, definition (provide definition id),
metadata: {display_name ..}, children (return display_name, anyfieldname, children (return
block if its children includes the one given value). If you want block if its children includes the one given value). If you want
substring matching use {$regex: /acme.*corp/i} type syntax. substring matching use {$regex: /acme.*corp/i} type syntax.
...@@ -371,7 +375,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -371,7 +375,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
course = self._lookup_course(locator) course = self._lookup_course(locator)
items = [] items = []
for parent_id, value in course['blocks'].iteritems(): for parent_id, value in course['blocks'].iteritems():
for child_id in value['children']: for child_id in value['fields'].get('children', []):
if locator.usage_id == child_id: if locator.usage_id == child_id:
items.append(BlockUsageLocator(url=locator.as_course_locator(), usage_id=parent_id)) items.append(BlockUsageLocator(url=locator.as_course_locator(), usage_id=parent_id))
return items return items
...@@ -427,11 +431,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -427,11 +431,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
definition = self.definitions.find_one({'_id': definition_locator.definition_id}) definition = self.definitions.find_one({'_id': definition_locator.definition_id})
if definition is None: if definition is None:
return None return None
return {'original_version': definition['original_version'], return definition['edit_info']
'previous_version': definition['previous_version'],
'edited_by': definition['edited_by'],
'edited_on': definition['edited_on']
}
def get_course_successors(self, course_locator, version_history_depth=1): def get_course_successors(self, course_locator, version_history_depth=1):
''' '''
...@@ -471,29 +471,29 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -471,29 +471,29 @@ class SplitMongoModuleStore(ModuleStoreBase):
Find the history of this block. Return as a VersionTree of each place the block changed (except Find the history of this block. Return as a VersionTree of each place the block changed (except
deletion). deletion).
The block's history tracks its explicit changes; so, changes in descendants won't be reflected The block's history tracks its explicit changes but not the changes in its children.
as new iterations.
''' '''
block_locator = block_locator.version_agnostic() block_locator = block_locator.version_agnostic()
course_struct = self._lookup_course(block_locator) course_struct = self._lookup_course(block_locator)
usage_id = block_locator.usage_id usage_id = block_locator.usage_id
update_version_field = 'blocks.{}.update_version'.format(usage_id) update_version_field = 'blocks.{}.edit_info.update_version'.format(usage_id)
all_versions_with_block = self.structures.find({'original_version': course_struct['original_version'], all_versions_with_block = self.structures.find({'original_version': course_struct['original_version'],
update_version_field: {'$exists': True}}) update_version_field: {'$exists': True}})
# find (all) root versions and build map previous: [successors] # find (all) root versions and build map previous: [successors]
possible_roots = [] possible_roots = []
result = {} result = {}
for version in all_versions_with_block: for version in all_versions_with_block:
if version['_id'] == version['blocks'][usage_id]['update_version']: if version['_id'] == version['blocks'][usage_id]['edit_info']['update_version']:
if version['blocks'][usage_id].get('previous_version') is None: if version['blocks'][usage_id]['edit_info'].get('previous_version') is None:
possible_roots.append(version['blocks'][usage_id]['update_version']) possible_roots.append(version['blocks'][usage_id]['edit_info']['update_version'])
else: else:
result.setdefault(version['blocks'][usage_id]['previous_version'], set()).add( result.setdefault(version['blocks'][usage_id]['edit_info']['previous_version'], set()).add(
version['blocks'][usage_id]['update_version']) version['blocks'][usage_id]['edit_info']['update_version'])
# more than one possible_root means usage was added and deleted > 1x. # more than one possible_root means usage was added and deleted > 1x.
if len(possible_roots) > 1: if len(possible_roots) > 1:
# find the history segment including block_locator's version # find the history segment including block_locator's version
element_to_find = course_struct['blocks'][usage_id]['update_version'] element_to_find = course_struct['blocks'][usage_id]['edit_info']['update_version']
if element_to_find in possible_roots: if element_to_find in possible_roots:
possible_roots = [element_to_find] possible_roots = [element_to_find]
for possibility in possible_roots: for possibility in possible_roots:
...@@ -513,7 +513,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -513,7 +513,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
Find the version_history_depth next versions of this definition. Return as a VersionTree Find the version_history_depth next versions of this definition. Return as a VersionTree
''' '''
# TODO implement # TODO implement
pass raise NotImplementedError()
def create_definition_from_data(self, new_def_data, category, user_id): def create_definition_from_data(self, new_def_data, category, user_id):
""" """
...@@ -522,16 +522,21 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -522,16 +522,21 @@ class SplitMongoModuleStore(ModuleStoreBase):
:param user_id: request.user object :param user_id: request.user object
""" """
document = {"category" : category, new_def_data = self._filter_special_fields(new_def_data)
"data": new_def_data, document = {
"category" : category,
"fields": new_def_data,
"edit_info": {
"edited_by": user_id, "edited_by": user_id,
"edited_on": datetime.datetime.utcnow(), "edited_on": datetime.datetime.now(UTC),
"previous_version": None, "previous_version": None,
"original_version": None} "original_version": None
}
}
new_id = self.definitions.insert(document) new_id = self.definitions.insert(document)
definition_locator = DescriptionLocator(new_id) definition_locator = DescriptionLocator(new_id)
document['original_version'] = new_id document['edit_info']['original_version'] = new_id
self.definitions.update({'_id': new_id}, {'$set': {"original_version": new_id}}) self.definitions.update({'_id': new_id}, {'$set': {"edit_info.original_version": new_id}})
return definition_locator return definition_locator
def update_definition_from_data(self, definition_locator, new_def_data, user_id): def update_definition_from_data(self, definition_locator, new_def_data, user_id):
...@@ -541,16 +546,14 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -541,16 +546,14 @@ class SplitMongoModuleStore(ModuleStoreBase):
:param user_id: request.user :param user_id: request.user
""" """
new_def_data = self._filter_special_fields(new_def_data)
def needs_saved(): def needs_saved():
if isinstance(new_def_data, dict):
for key, value in new_def_data.iteritems(): for key, value in new_def_data.iteritems():
if key not in old_definition['data'] or value != old_definition['data'][key]: if key not in old_definition['fields'] or value != old_definition['fields'][key]:
return True return True
for key, value in old_definition['data'].iteritems(): for key, value in old_definition.get('fields', {}).iteritems():
if key not in new_def_data: if key not in new_def_data:
return True return True
else:
return new_def_data != old_definition['data']
# if this looks in cache rather than fresh fetches, then it will probably not detect # if this looks in cache rather than fresh fetches, then it will probably not detect
# actual change b/c the descriptor and cache probably point to the same objects # actual change b/c the descriptor and cache probably point to the same objects
...@@ -560,10 +563,10 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -560,10 +563,10 @@ class SplitMongoModuleStore(ModuleStoreBase):
del old_definition['_id'] del old_definition['_id']
if needs_saved(): if needs_saved():
old_definition['data'] = new_def_data old_definition['fields'] = new_def_data
old_definition['edited_by'] = user_id old_definition['edit_info']['edited_by'] = user_id
old_definition['edited_on'] = datetime.datetime.utcnow() old_definition['edit_info']['edited_on'] = datetime.datetime.now(UTC)
old_definition['previous_version'] = definition_locator.definition_id old_definition['edit_info']['previous_version'] = definition_locator.definition_id
new_id = self.definitions.insert(old_definition) new_id = self.definitions.insert(old_definition)
return DescriptionLocator(new_id), True return DescriptionLocator(new_id), True
else: else:
...@@ -605,11 +608,11 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -605,11 +608,11 @@ class SplitMongoModuleStore(ModuleStoreBase):
else: else:
return id_root return id_root
# TODO I would love to write this to take a real descriptor and persist it BUT descriptors, kvs, and dbmodel # TODO Should I rewrite this to take a new xblock instance rather than to construct it? That is, require the
# all assume locators are set and unique! Having this take the model contents piecemeal breaks the separation # caller to use XModuleDescriptor.load_from_json thus reducing similar code and making the object creation and
# of model from persistence layer # validation behavior a responsibility of the model layer rather than the persistence layer.
def create_item(self, course_or_parent_locator, category, user_id, definition_locator=None, new_def_data=None, def create_item(self, course_or_parent_locator, category, user_id, definition_locator=None, fields=None,
metadata=None, force=False): force=False):
""" """
Add a descriptor to persistence as the last child of the optional parent_location or just as an element Add a descriptor to persistence as the last child of the optional parent_location or just as an element
of the course (if no parent provided). Return the resulting post saved version with populated locators. of the course (if no parent provided). Return the resulting post saved version with populated locators.
...@@ -624,9 +627,10 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -624,9 +627,10 @@ class SplitMongoModuleStore(ModuleStoreBase):
The incoming definition_locator should either be None to indicate this is a brand new definition or The incoming definition_locator should either be None to indicate this is a brand new definition or
a pointer to the existing definition to which this block should point or from which this was derived. a pointer to the existing definition to which this block should point or from which this was derived.
If new_def_data is None, then definition_locator must have a value meaning that this block points If fields does not contain any Scope.content, then definition_locator must have a value meaning that this
to the existing definition. If new_def_data is not None and definition_location is not None, then block points
new_def_data is assumed to be a new payload for definition_location. to the existing definition. If fields contains Scope.content and definition_locator is not None, then
the Scope.content fields are assumed to be a new payload for definition_locator.
Creates a new version of the course structure, creates and inserts the new block, makes the block point Creates a new version of the course structure, creates and inserts the new block, makes the block point
to the definition which may be new or a new version of an existing or an existing. to the definition which may be new or a new version of an existing or an existing.
...@@ -645,6 +649,8 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -645,6 +649,8 @@ class SplitMongoModuleStore(ModuleStoreBase):
index_entry = self._get_index_if_valid(course_or_parent_locator, force) index_entry = self._get_index_if_valid(course_or_parent_locator, force)
structure = self._lookup_course(course_or_parent_locator) structure = self._lookup_course(course_or_parent_locator)
partitioned_fields = self._partition_fields_by_scope(category, fields)
new_def_data = partitioned_fields.get(Scope.content, {})
# persist the definition if persisted != passed # persist the definition if persisted != passed
if (definition_locator is None or definition_locator.definition_id is None): if (definition_locator is None or definition_locator.definition_id is None):
definition_locator = self.create_definition_from_data(new_def_data, category, user_id) definition_locator = self.create_definition_from_data(new_def_data, category, user_id)
...@@ -655,23 +661,27 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -655,23 +661,27 @@ class SplitMongoModuleStore(ModuleStoreBase):
new_structure = self._version_structure(structure, user_id) new_structure = self._version_structure(structure, user_id)
# generate an id # generate an id
new_usage_id = self._generate_usage_id(new_structure['blocks'], category) new_usage_id = self._generate_usage_id(new_structure['blocks'], category)
update_version_keys = ['blocks.{}.update_version'.format(new_usage_id)] update_version_keys = ['blocks.{}.edit_info.update_version'.format(new_usage_id)]
if isinstance(course_or_parent_locator, BlockUsageLocator) and course_or_parent_locator.usage_id is not None: if isinstance(course_or_parent_locator, BlockUsageLocator) and course_or_parent_locator.usage_id is not None:
parent = new_structure['blocks'][course_or_parent_locator.usage_id] parent = new_structure['blocks'][course_or_parent_locator.usage_id]
parent['children'].append(new_usage_id) parent['fields'].setdefault('children', []).append(new_usage_id)
parent['edited_on'] = datetime.datetime.utcnow() parent['edit_info']['edited_on'] = datetime.datetime.now(UTC)
parent['edited_by'] = user_id parent['edit_info']['edited_by'] = user_id
parent['previous_version'] = parent['update_version'] parent['edit_info']['previous_version'] = parent['edit_info']['update_version']
update_version_keys.append('blocks.{}.update_version'.format(course_or_parent_locator.usage_id)) update_version_keys.append('blocks.{}.edit_info.update_version'.format(course_or_parent_locator.usage_id))
block_fields = partitioned_fields.get(Scope.settings, {})
if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children])
new_structure['blocks'][new_usage_id] = { new_structure['blocks'][new_usage_id] = {
"children": [],
"category": category, "category": category,
"definition": definition_locator.definition_id, "definition": definition_locator.definition_id,
"metadata": metadata if metadata else {}, "fields": block_fields,
'edited_on': datetime.datetime.utcnow(), 'edit_info': {
'edited_on': datetime.datetime.now(UTC),
'edited_by': user_id, 'edited_by': user_id,
'previous_version': None 'previous_version': None
} }
}
new_id = self.structures.insert(new_structure) new_id = self.structures.insert(new_structure)
update_version_payload = {key: new_id for key in update_version_keys} update_version_payload = {key: new_id for key in update_version_keys}
self.structures.update({'_id': new_id}, self.structures.update({'_id': new_id},
...@@ -689,8 +699,9 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -689,8 +699,9 @@ class SplitMongoModuleStore(ModuleStoreBase):
usage_id=new_usage_id, usage_id=new_usage_id,
version_guid=new_id)) version_guid=new_id))
def create_course(self, org, prettyid, user_id, id_root=None, metadata=None, course_data=None, def create_course(
master_version='draft', versions_dict=None, root_category='course'): self, org, prettyid, user_id, id_root=None, fields=None,
master_branch='draft', versions_dict=None, root_category='course'):
""" """
Create a new entry in the active courses index which points to an existing or new structure. Returns Create a new entry in the active courses index which points to an existing or new structure. Returns
the course root of the resulting entry (the location has the course id) the course root of the resulting entry (the location has the course id)
...@@ -698,93 +709,106 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -698,93 +709,106 @@ class SplitMongoModuleStore(ModuleStoreBase):
id_root: allows the caller to specify the course_id. It's a root in that, if it's already taken, id_root: allows the caller to specify the course_id. It's a root in that, if it's already taken,
this method will append things to the root to make it unique. (defaults to org) this method will append things to the root to make it unique. (defaults to org)
metadata: if provided, will set the metadata of the root course object in the new draft course. If both fields: if scope.settings fields provided, will set the fields of the root course object in the
metadata and a starting version are provided, it will generate a successor version to the given version, new course. If both
and update the metadata with any provided values (via update not setting). settings fields and a starting version are provided (via versions_dict), it will generate a successor version
to the given version,
and update the settings fields with any provided values (via update not setting).
course_data: if provided, will update the data of the new course xblock definition to this. Like metadata, fields (content): if scope.content fields provided, will update the fields of the new course
xblock definition to this. Like settings fields,
if provided, this will cause a new version of any given version as well as a new version of the if provided, this will cause a new version of any given version as well as a new version of the
definition (which will point to the existing one if given a version). If not provided and given definition (which will point to the existing one if given a version). If not provided and given
a draft_version, it will reuse the same definition as the draft course (obvious since it's reusing the draft a version_dict, it will reuse the same definition as that version's course
course). If not provided and no draft is given, it will be empty and get the field defaults (hopefully) when (obvious since it's reusing the
course). If not provided and no version_dict is given, it will be empty and get the field defaults
when
loaded. loaded.
master_version: the tag (key) for the version name in the dict which is the 'draft' version. Not the actual master_branch: the tag (key) for the version name in the dict which is the 'draft' version. Not the actual
version guid, but what to call it. version guid, but what to call it.
versions_dict: the starting version ids where the keys are the tags such as 'draft' and 'published' versions_dict: the starting version ids where the keys are the tags such as 'draft' and 'published'
and the values are structure guids. If provided, the new course will reuse this version (unless you also and the values are structure guids. If provided, the new course will reuse this version (unless you also
provide any overrides such as metadata, see above). if not provided, will create a mostly empty course provide any fields overrides, see above). if not provided, will create a mostly empty course
structure with just a category course root xblock. structure with just a category course root xblock.
""" """
if metadata is None: partitioned_fields = self._partition_fields_by_scope('course', fields)
metadata = {} block_fields = partitioned_fields.setdefault(Scope.settings, {})
if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children])
definition_fields = self._filter_special_fields(partitioned_fields.get(Scope.content, {}))
# build from inside out: definition, structure, index entry # build from inside out: definition, structure, index entry
# if building a wholly new structure # if building a wholly new structure
if versions_dict is None or master_version not in versions_dict: if versions_dict is None or master_branch not in versions_dict:
# create new definition and structure # create new definition and structure
if course_data is None:
course_data = {}
definition_entry = { definition_entry = {
'category': root_category, 'category': root_category,
'data': course_data, 'fields': definition_fields,
'edit_info': {
'edited_by': user_id, 'edited_by': user_id,
'edited_on': datetime.datetime.utcnow(), 'edited_on': datetime.datetime.now(UTC),
'previous_version': None, 'previous_version': None,
} }
}
definition_id = self.definitions.insert(definition_entry) definition_id = self.definitions.insert(definition_entry)
definition_entry['original_version'] = definition_id definition_entry['edit_info']['original_version'] = definition_id
self.definitions.update({'_id': definition_id}, {'$set': {"original_version": definition_id}}) self.definitions.update({'_id': definition_id}, {'$set': {"edit_info.original_version": definition_id}})
draft_structure = { draft_structure = {
'root': 'course', 'root': 'course',
'previous_version': None, 'previous_version': None,
'edited_by': user_id, 'edited_by': user_id,
'edited_on': datetime.datetime.utcnow(), 'edited_on': datetime.datetime.now(UTC),
'blocks': { 'blocks': {
'course': { 'course': {
'children':[],
'category': 'course', 'category': 'course',
'definition': definition_id, 'definition': definition_id,
'metadata': metadata, 'fields': block_fields,
'edited_on': datetime.datetime.utcnow(), 'edit_info': {
'edited_on': datetime.datetime.now(UTC),
'edited_by': user_id, 'edited_by': user_id,
'previous_version': None}}} 'previous_version': None
}
}
}
}
new_id = self.structures.insert(draft_structure) new_id = self.structures.insert(draft_structure)
draft_structure['original_version'] = new_id draft_structure['original_version'] = new_id
self.structures.update({'_id': new_id}, self.structures.update({'_id': new_id},
{'$set': {"original_version": new_id, {'$set': {"original_version": new_id,
'blocks.course.update_version': new_id}}) 'blocks.course.edit_info.update_version': new_id}})
if versions_dict is None: if versions_dict is None:
versions_dict = {master_version: new_id} versions_dict = {master_branch: new_id}
else: else:
versions_dict[master_version] = new_id versions_dict[master_branch] = new_id
else: else:
# just get the draft_version structure # just get the draft_version structure
draft_version = CourseLocator(version_guid=versions_dict[master_version]) draft_version = CourseLocator(version_guid=versions_dict[master_branch])
draft_structure = self._lookup_course(draft_version) draft_structure = self._lookup_course(draft_version)
if course_data is not None or metadata: if definition_fields or block_fields:
draft_structure = self._version_structure(draft_structure, user_id) draft_structure = self._version_structure(draft_structure, user_id)
root_block = draft_structure['blocks'][draft_structure['root']] root_block = draft_structure['blocks'][draft_structure['root']]
if metadata is not None: if block_fields is not None:
root_block['metadata'].update(metadata) root_block['fields'].update(block_fields)
if course_data is not None: if definition_fields is not None:
definition = self.definitions.find_one({'_id': root_block['definition']}) definition = self.definitions.find_one({'_id': root_block['definition']})
definition['data'].update(course_data) definition['fields'].update(definition_fields)
definition['previous_version'] = definition['_id'] definition['edit_info']['previous_version'] = definition['_id']
definition['edited_by'] = user_id definition['edit_info']['edited_by'] = user_id
definition['edited_on'] = datetime.datetime.utcnow() definition['edit_info']['edited_on'] = datetime.datetime.now(UTC)
del definition['_id'] del definition['_id']
root_block['definition'] = self.definitions.insert(definition) root_block['definition'] = self.definitions.insert(definition)
root_block['edited_on'] = datetime.datetime.utcnow() root_block['edit_info']['edited_on'] = datetime.datetime.now(UTC)
root_block['edited_by'] = user_id root_block['edit_info']['edited_by'] = user_id
root_block['previous_version'] = root_block.get('update_version') root_block['edit_info']['previous_version'] = root_block['edit_info'].get('update_version')
# insert updates the '_id' in draft_structure # insert updates the '_id' in draft_structure
new_id = self.structures.insert(draft_structure) new_id = self.structures.insert(draft_structure)
versions_dict[master_version] = new_id versions_dict[master_branch] = new_id
self.structures.update({'_id': new_id}, self.structures.update({'_id': new_id},
{'$set': {'blocks.{}.update_version'.format(draft_structure['root']): new_id}}) {'$set': {'blocks.{}.edit_info.update_version'.format(draft_structure['root']): new_id}})
# create the index entry # create the index entry
if id_root is None: if id_root is None:
id_root = org id_root = org
...@@ -795,14 +819,14 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -795,14 +819,14 @@ class SplitMongoModuleStore(ModuleStoreBase):
'org': org, 'org': org,
'prettyid': prettyid, 'prettyid': prettyid,
'edited_by': user_id, 'edited_by': user_id,
'edited_on': datetime.datetime.utcnow(), 'edited_on': datetime.datetime.now(UTC),
'versions': versions_dict} 'versions': versions_dict}
new_id = self.course_index.insert(index_entry) new_id = self.course_index.insert(index_entry)
return self.get_course(CourseLocator(course_id=new_id, branch=master_version)) return self.get_course(CourseLocator(course_id=new_id, branch=master_branch))
def update_item(self, descriptor, user_id, force=False): def update_item(self, descriptor, user_id, force=False):
""" """
Save the descriptor's definition, metadata, & children references (i.e., it doesn't descend the tree). Save the descriptor's fields. it doesn't descend the course dag to save the children.
Return the new descriptor (updated location). Return the new descriptor (updated location).
raises ItemNotFoundError if the location does not exist. raises ItemNotFoundError if the location does not exist.
...@@ -819,31 +843,38 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -819,31 +843,38 @@ class SplitMongoModuleStore(ModuleStoreBase):
index_entry = self._get_index_if_valid(descriptor.location, force) index_entry = self._get_index_if_valid(descriptor.location, force)
descriptor.definition_locator, is_updated = self.update_definition_from_data( descriptor.definition_locator, is_updated = self.update_definition_from_data(
descriptor.definition_locator, descriptor.xblock_kvs.get_data(), user_id) descriptor.definition_locator, descriptor.get_explicitly_set_fields_by_scope(Scope.content), user_id)
# check children # check children
original_entry = original_structure['blocks'][descriptor.location.usage_id] original_entry = original_structure['blocks'][descriptor.location.usage_id]
if (not is_updated and descriptor.has_children if (not is_updated and descriptor.has_children
and not self._xblock_lists_equal(original_entry['children'], descriptor.children)): and not self._xblock_lists_equal(original_entry['fields']['children'], descriptor.children)):
is_updated = True is_updated = True
# check metadata # check metadata
if not is_updated: if not is_updated:
is_updated = self._compare_metadata(descriptor.xblock_kvs.get_own_metadata(), original_entry['metadata']) is_updated = self._compare_settings(
descriptor.get_explicitly_set_fields_by_scope(Scope.settings),
original_entry['fields']
)
# if updated, rev the structure # if updated, rev the structure
if is_updated: if is_updated:
new_structure = self._version_structure(original_structure, user_id) new_structure = self._version_structure(original_structure, user_id)
block_data = new_structure['blocks'][descriptor.location.usage_id] block_data = new_structure['blocks'][descriptor.location.usage_id]
if descriptor.has_children:
block_data["children"] = [self._usage_id(child) for child in descriptor.children]
block_data["definition"] = descriptor.definition_locator.definition_id block_data["definition"] = descriptor.definition_locator.definition_id
block_data["metadata"] = descriptor.xblock_kvs.get_own_metadata() block_data["fields"] = descriptor.get_explicitly_set_fields_by_scope(Scope.settings)
block_data['edited_on'] = datetime.datetime.utcnow() if descriptor.has_children:
block_data['edited_by'] = user_id block_data['fields']["children"] = [self._usage_id(child) for child in descriptor.children]
block_data['previous_version'] = block_data['update_version']
block_data['edit_info'] = {
'edited_on': datetime.datetime.now(UTC),
'edited_by': user_id,
'previous_version': block_data['edit_info']['update_version'],
}
new_id = self.structures.insert(new_structure) new_id = self.structures.insert(new_structure)
self.structures.update({'_id': new_id}, self.structures.update(
{'$set': {'blocks.{}.update_version'.format(descriptor.location.usage_id): new_id}}) {'_id': new_id},
{'$set': {'blocks.{}.edit_info.update_version'.format(descriptor.location.usage_id): new_id}})
# update the index entry if appropriate # update the index entry if appropriate
if index_entry is not None: if index_entry is not None:
...@@ -869,8 +900,8 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -869,8 +900,8 @@ class SplitMongoModuleStore(ModuleStoreBase):
returns the post-persisted version of the incoming xblock. Note that its children will be ids not returns the post-persisted version of the incoming xblock. Note that its children will be ids not
objects. objects.
:param xblock: :param xblock: the head of the dag
:param user_id: :param user_id: who's doing the change
""" """
# find course_index entry if applicable and structures entry # find course_index entry if applicable and structures entry
index_entry = self._get_index_if_valid(xblock.location, force) index_entry = self._get_index_if_valid(xblock.location, force)
...@@ -883,7 +914,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -883,7 +914,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
new_id = self.structures.insert(new_structure) new_id = self.structures.insert(new_structure)
update_command = {} update_command = {}
for usage_id in changed_blocks: for usage_id in changed_blocks:
update_command['blocks.{}.update_version'.format(usage_id)] = new_id update_command['blocks.{}.edit_info.update_version'.format(usage_id)] = new_id
self.structures.update({'_id': new_id}, {'$set': update_command}) self.structures.update({'_id': new_id}, {'$set': update_command})
# update the index entry if appropriate # update the index entry if appropriate
...@@ -897,14 +928,14 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -897,14 +928,14 @@ class SplitMongoModuleStore(ModuleStoreBase):
def _persist_subdag(self, xblock, user_id, structure_blocks): def _persist_subdag(self, xblock, user_id, structure_blocks):
# persist the definition if persisted != passed # persist the definition if persisted != passed
new_def_data = xblock.xblock_kvs.get_data() new_def_data = self._filter_special_fields(xblock.get_explicitly_set_fields_by_scope(Scope.content))
if (xblock.definition_locator is None or xblock.definition_locator.definition_id is None): if (xblock.definition_locator is None or xblock.definition_locator.definition_id is None):
xblock.definition_locator = self.create_definition_from_data(new_def_data, xblock.definition_locator = self.create_definition_from_data(
xblock.category, user_id) new_def_data, xblock.category, user_id)
is_updated = True is_updated = True
elif new_def_data is not None: elif new_def_data:
xblock.definition_locator, is_updated = self.update_definition_from_data(xblock.definition_locator, xblock.definition_locator, is_updated = self.update_definition_from_data(
new_def_data, user_id) xblock.definition_locator, new_def_data, user_id)
if xblock.location.usage_id is None: if xblock.location.usage_id is None:
# generate an id # generate an id
...@@ -916,7 +947,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -916,7 +947,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
is_new = False is_new = False
usage_id = xblock.location.usage_id usage_id = xblock.location.usage_id
if (not is_updated and xblock.has_children if (not is_updated and xblock.has_children
and not self._xblock_lists_equal(structure_blocks[usage_id]['children'], xblock.children)): and not self._xblock_lists_equal(structure_blocks[usage_id]['fields']['children'], xblock.children)):
is_updated = True is_updated = True
children = [] children = []
...@@ -930,41 +961,52 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -930,41 +961,52 @@ class SplitMongoModuleStore(ModuleStoreBase):
children.append(child) children.append(child)
is_updated = is_updated or updated_blocks is_updated = is_updated or updated_blocks
metadata = xblock.xblock_kvs.get_own_metadata() block_fields = xblock.get_explicitly_set_fields_by_scope(Scope.settings)
if not is_new and not is_updated: if not is_new and not is_updated:
is_updated = self._compare_metadata(metadata, structure_blocks[usage_id]['metadata']) is_updated = self._compare_settings(block_fields, structure_blocks[usage_id]['fields'])
if children:
block_fields['children'] = children
if is_updated: if is_updated:
previous_version = None if is_new else structure_blocks[usage_id]['edit_info'].get('update_version')
structure_blocks[usage_id] = { structure_blocks[usage_id] = {
"children": children,
"category": xblock.category, "category": xblock.category,
"definition": xblock.definition_locator.definition_id, "definition": xblock.definition_locator.definition_id,
"metadata": metadata if metadata else {}, "fields": block_fields,
'previous_version': structure_blocks.get(usage_id, {}).get('update_version'), 'edit_info': {
'previous_version': previous_version,
'edited_by': user_id, 'edited_by': user_id,
'edited_on': datetime.datetime.utcnow() 'edited_on': datetime.datetime.now(UTC)
}
} }
updated_blocks.append(usage_id) updated_blocks.append(usage_id)
return updated_blocks return updated_blocks
def _compare_metadata(self, metadata, original_metadata): def _compare_settings(self, settings, original_fields):
original_keys = original_metadata.keys() """
if len(metadata) != len(original_keys): Return True if the settings are not == to the original fields
:param settings:
:param original_fields:
"""
original_keys = original_fields.keys()
if 'children' in original_keys:
original_keys.remove('children')
if len(settings) != len(original_keys):
return True return True
else: else:
new_keys = metadata.keys() new_keys = settings.keys()
for key in original_keys: for key in original_keys:
if key not in new_keys or original_metadata[key] != metadata[key]: if key not in new_keys or original_fields[key] != settings[key]:
return True return True
# TODO change all callers to update_item def update_children(self, location, children):
def update_children(self, course_id, location, children): '''Deprecated, use update_item.'''
raise NotImplementedError() raise NotImplementedError('use update_item')
# TODO change all callers to update_item def update_metadata(self, location, metadata):
def update_metadata(self, course_id, location, metadata): '''Deprecated, use update_item.'''
raise NotImplementedError() raise NotImplementedError('use update_item')
def update_course_index(self, course_locator, new_values_dict, update_versions=False): def update_course_index(self, course_locator, new_values_dict, update_versions=False):
""" """
...@@ -992,9 +1034,9 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -992,9 +1034,9 @@ class SplitMongoModuleStore(ModuleStoreBase):
self.course_index.update({'_id': course_locator.course_id}, self.course_index.update({'_id': course_locator.course_id},
{'$set': new_values_dict}) {'$set': new_values_dict})
def delete_item(self, usage_locator, user_id, force=False): def delete_item(self, usage_locator, user_id, delete_children=False, force=False):
""" """
Delete the tree rooted at block and any references w/in the course to the block Delete the block or tree rooted at block (if delete_children) and any references w/in the course to the block
from a new version of the course structure. from a new version of the course structure.
returns CourseLocator for new version returns CourseLocator for new version
...@@ -1018,16 +1060,17 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1018,16 +1060,17 @@ class SplitMongoModuleStore(ModuleStoreBase):
update_version_keys = [] update_version_keys = []
for parent in parents: for parent in parents:
parent_block = new_blocks[parent.usage_id] parent_block = new_blocks[parent.usage_id]
parent_block['children'].remove(usage_locator.usage_id) parent_block['fields']['children'].remove(usage_locator.usage_id)
parent_block['edited_on'] = datetime.datetime.utcnow() parent_block['edit_info']['edited_on'] = datetime.datetime.now(UTC)
parent_block['edited_by'] = user_id parent_block['edit_info']['edited_by'] = user_id
parent_block['previous_version'] = parent_block['update_version'] parent_block['edit_info']['previous_version'] = parent_block['edit_info']['update_version']
update_version_keys.append('blocks.{}.update_version'.format(parent.usage_id)) update_version_keys.append('blocks.{}.edit_info.update_version'.format(parent.usage_id))
# remove subtree # remove subtree
def remove_subtree(usage_id): def remove_subtree(usage_id):
for child in new_blocks[usage_id]['children']: for child in new_blocks[usage_id]['fields'].get('children', []):
remove_subtree(child) remove_subtree(child)
del new_blocks[usage_id] del new_blocks[usage_id]
if delete_children:
remove_subtree(usage_locator.usage_id) remove_subtree(usage_locator.usage_id)
# update index if appropriate and structures # update index if appropriate and structures
...@@ -1062,32 +1105,38 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1062,32 +1105,38 @@ class SplitMongoModuleStore(ModuleStoreBase):
# this is the only real delete in the system. should it do something else? # this is the only real delete in the system. should it do something else?
self.course_index.remove(index['_id']) self.course_index.remove(index['_id'])
def inherit_metadata(self, block_map, block, inheriting_metadata=None): def get_errored_courses(self):
"""
This function doesn't make sense for the mongo modulestore, as structures
are loaded on demand, rather than up front
"""
return {}
def inherit_settings(self, block_map, block, inheriting_settings=None):
""" """
Updates block with any value Updates block with any inheritable setting set by an ancestor and recurses to children.
that exist in inheriting_metadata and don't appear in block['metadata'],
and then inherits block['metadata'] to all of the children in
block['children']. Filters by inheritance.INHERITABLE_METADATA
""" """
if block is None: if block is None:
return return
if inheriting_metadata is None: if inheriting_settings is None:
inheriting_metadata = {} inheriting_settings = {}
# the currently passed down values take precedence over any previously cached ones # the currently passed down values take precedence over any previously cached ones
# NOTE: this should show the values which all fields would have if inherited: i.e., # NOTE: this should show the values which all fields would have if inherited: i.e.,
# not set to the locally defined value but to value set by nearest ancestor who sets it # not set to the locally defined value but to value set by nearest ancestor who sets it
block.setdefault('_inherited_metadata', {}).update(inheriting_metadata) # ALSO NOTE: no xblock should ever define a _inherited_settings field as it will collide w/ this logic.
block.setdefault('_inherited_settings', {}).update(inheriting_settings)
# update the inheriting w/ what should pass to children # update the inheriting w/ what should pass to children
inheriting_metadata = block['_inherited_metadata'].copy() inheriting_settings = block['_inherited_settings'].copy()
block_fields = block['fields']
for field in inheritance.INHERITABLE_METADATA: for field in inheritance.INHERITABLE_METADATA:
if field in block['metadata']: if field in block_fields:
inheriting_metadata[field] = block['metadata'][field] inheriting_settings[field] = block_fields[field]
for child in block.get('children', []): for child in block_fields.get('children', []):
self.inherit_metadata(block_map, block_map[child], inheriting_metadata) self.inherit_settings(block_map, block_map[child], inheriting_settings)
def descendants(self, block_map, usage_id, depth, descendent_map): def descendants(self, block_map, usage_id, depth, descendent_map):
""" """
...@@ -1104,7 +1153,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1104,7 +1153,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
if depth is None or depth > 0: if depth is None or depth > 0:
depth = depth - 1 if depth is not None else None depth = depth - 1 if depth is not None else None
for child in block_map[usage_id].get('children', []): for child in block_map[usage_id]['fields'].get('children', []):
descendent_map = self.descendants(block_map, child, depth, descendent_map = self.descendants(block_map, child, depth,
descendent_map) descendent_map)
...@@ -1217,7 +1266,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1217,7 +1266,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
del new_structure['_id'] del new_structure['_id']
new_structure['previous_version'] = structure['_id'] new_structure['previous_version'] = structure['_id']
new_structure['edited_by'] = user_id new_structure['edited_by'] = user_id
new_structure['edited_on'] = datetime.datetime.utcnow() new_structure['edited_on'] = datetime.datetime.now(UTC)
return new_structure return new_structure
def _find_local_root(self, element_to_find, possibility, tree): def _find_local_root(self, element_to_find, possibility, tree):
...@@ -1242,3 +1291,31 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1242,3 +1291,31 @@ class SplitMongoModuleStore(ModuleStoreBase):
self.course_index.update( self.course_index.update(
{"_id": index_entry["_id"]}, {"_id": index_entry["_id"]},
{"$set": {"versions.{}".format(branch): new_id}}) {"$set": {"versions.{}".format(branch): new_id}})
def _partition_fields_by_scope(self, category, fields):
"""
Return dictionary of {scope: {field1: val, ..}..} for the fields of this potential xblock
:param category: the xblock category
:param fields: the dictionary of {fieldname: value}
"""
if fields is None:
return {}
cls = XModuleDescriptor.load_class(category)
result = collections.defaultdict(dict)
for field_name, value in fields.iteritems():
field = getattr(cls, field_name)
result[field.scope][field_name] = value
return result
def _filter_special_fields(self, fields):
"""
Remove any fields which split or its kvs computes or adds but does not want persisted.
:param fields: a dict of fields
"""
if 'location' in fields:
del fields['location']
if 'category' in fields:
del fields['category']
return fields
...@@ -8,45 +8,49 @@ from .definition_lazy_loader import DefinitionLazyLoader ...@@ -8,45 +8,49 @@ from .definition_lazy_loader import DefinitionLazyLoader
SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id') SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id')
# TODO should this be here or w/ x_module or ??? PROVENANCE_LOCAL = 'local'
PROVENANCE_DEFAULT = 'default'
PROVENANCE_INHERITED = 'inherited'
class SplitMongoKVS(KeyValueStore): class SplitMongoKVS(KeyValueStore):
""" """
A KeyValueStore that maps keyed data access to one of the 3 data areas A KeyValueStore that maps keyed data access to one of the 3 data areas
known to the MongoModuleStore (data, children, and metadata) known to the MongoModuleStore (data, children, and metadata)
""" """
def __init__(self, definition, children, metadata, _inherited_metadata, location, category):
def __init__(self, definition, fields, _inherited_settings, location, category):
""" """
:param definition: :param definition: either a lazyloader or definition id for the definition
:param children: :param fields: a dictionary of the locally set fields
:param metadata: the locally defined value for each metadata field :param _inherited_settings: the value of each inheritable field from above this.
:param _inherited_metadata: the value of each inheritable field from above this. Note, local fields may override and disagree w/ this b/c this says what the value
Note, metadata may override and disagree w/ this b/c this says what the value should be if the field is undefined.
should be if metadata is undefined for this field.
""" """
# ensure kvs's don't share objects w/ others so that changes can't appear in separate ones # ensure kvs's don't share objects w/ others so that changes can't appear in separate ones
# the particular use case was that changes to kvs's were polluting caches. My thinking was # the particular use case was that changes to kvs's were polluting caches. My thinking was
# that kvs's should be independent thus responsible for the isolation. # that kvs's should be independent thus responsible for the isolation.
if isinstance(definition, DefinitionLazyLoader): self._definition = definition # either a DefinitionLazyLoader or the db id of the definition.
self._definition = definition # if the db id, then the definition is presumed to be loaded into _fields
else: self._fields = copy.copy(fields)
self._definition = copy.copy(definition) self._inherited_settings = _inherited_settings
self._children = copy.copy(children)
self._metadata = copy.copy(metadata)
self._inherited_metadata = _inherited_metadata
self._location = location self._location = location
self._category = category self._category = category
def get(self, key): def get(self, key):
if key.scope == Scope.children: # simplest case, field is directly set
return self._children if key.field_name in self._fields:
elif key.scope == Scope.parent: return self._fields[key.field_name]
# parent undefined in editing runtime (I think)
if key.scope == Scope.parent:
return None return None
if key.scope == Scope.children:
raise KeyError()
elif key.scope == Scope.settings: elif key.scope == Scope.settings:
if key.field_name in self._metadata: # get from inheritance since not locally set
return self._metadata[key.field_name] if key.field_name in self._inherited_settings:
elif key.field_name in self._inherited_metadata: return self._inherited_settings[key.field_name]
return self._inherited_metadata[key.field_name]
else: else:
raise KeyError() raise KeyError()
elif key.scope == Scope.content: elif key.scope == Scope.content:
...@@ -54,110 +58,118 @@ class SplitMongoKVS(KeyValueStore): ...@@ -54,110 +58,118 @@ class SplitMongoKVS(KeyValueStore):
return self._location return self._location
elif key.field_name == 'category': elif key.field_name == 'category':
return self._category return self._category
else: elif isinstance(self._definition, DefinitionLazyLoader):
if isinstance(self._definition, DefinitionLazyLoader): self._load_definition()
self._definition = self._definition.fetch() if key.field_name in self._fields:
if (key.field_name == 'data' and return self._fields[key.field_name]
not isinstance(self._definition.get('data'), dict)):
return self._definition.get('data')
elif 'data' not in self._definition or key.field_name not in self._definition['data']:
raise KeyError() raise KeyError()
else: else:
return self._definition['data'][key.field_name]
else:
raise InvalidScopeError(key.scope) raise InvalidScopeError(key.scope)
def set(self, key, value): def set(self, key, value):
# TODO cache db update implications & add method to invoke # handle any special cases
if key.scope == Scope.children: if key.scope not in [Scope.children, Scope.settings, Scope.content]:
self._children = value raise InvalidScopeError(key.scope)
# TODO remove inheritance from any orphaned exchildren if key.scope == Scope.content:
# TODO add inheritance to any new children
elif key.scope == Scope.settings:
# TODO if inheritable, push down to children who don't override
self._metadata[key.field_name] = value
elif key.scope == Scope.content:
if key.field_name == 'location': if key.field_name == 'location':
self._location = value self._location = value # is changing this legal?
return
elif key.field_name == 'category': elif key.field_name == 'category':
self._category = value # TODO should this raise an exception? that is, should xblock types be mungable?
else: return
if isinstance(self._definition, DefinitionLazyLoader):
self._definition = self._definition.fetch()
if (key.field_name == 'data' and
not isinstance(self._definition.get('data'), dict)):
self._definition.get('data')
else: else:
self._definition.setdefault('data', {})[key.field_name] = value self._load_definition()
else:
raise InvalidScopeError(key.scope) # set the field
self._fields[key.field_name] = value
# handle any side effects
# if key.scope == Scope.children:
# TODO remove inheritance from any exchildren
# TODO add inheritance to any new children
# if key.scope == Scope.settings:
# TODO if inheritable, push down to children
def delete(self, key): def delete(self, key):
# TODO cache db update implications & add method to invoke # handle any special cases
if key.scope == Scope.children: if key.scope not in [Scope.children, Scope.settings, Scope.content]:
self._children = [] raise InvalidScopeError(key.scope)
elif key.scope == Scope.settings: if key.scope == Scope.content:
# TODO if inheritable, ensure _inherited_metadata has value from above and
# revert children to that value
if key.field_name in self._metadata:
del self._metadata[key.field_name]
elif key.scope == Scope.content:
# don't allow deletion of location nor category
if key.field_name == 'location': if key.field_name == 'location':
pass return # noop
elif key.field_name == 'category': elif key.field_name == 'category':
pass # TODO should this raise an exception? that is, should xblock types be mungable?
return # noop
else: else:
if isinstance(self._definition, DefinitionLazyLoader): self._load_definition()
self._definition = self._definition.fetch()
if (key.field_name == 'data' and # delete the field value
not isinstance(self._definition.get('data'), dict)): if key.field_name in self._fields:
self._definition.setdefault('data', None) del self._fields[key.field_name]
else:
try: # handle any side effects
del self._definition['data'][key.field_name] # if key.scope == Scope.children:
except KeyError: # TODO remove inheritance from any exchildren
pass # if key.scope == Scope.settings:
else: # TODO if inheritable, push down _inherited_settings value to children
raise InvalidScopeError(key.scope)
def has(self, key): def has(self, key):
if key.scope in (Scope.children, Scope.parent): # handle any special cases
return True if key.scope == Scope.content:
elif key.scope == Scope.settings:
return key.field_name in self._metadata or key.field_name in self._inherited_metadata
elif key.scope == Scope.content:
if key.field_name == 'location': if key.field_name == 'location':
return True return True
elif key.field_name == 'category': elif key.field_name == 'category':
return self._category is not None return self._category is not None
else: else:
if isinstance(self._definition, DefinitionLazyLoader): self._load_definition()
self._definition = self._definition.fetch() elif key.scope == Scope.parent:
if (key.field_name == 'data' and return True
not isinstance(self._definition.get('data'), dict)):
return self._definition.get('data') is not None # it's not clear whether inherited values should return True. Right now they don't
else: # if someone changes it so that they do, then change any tests of field.name in xx._model_data
return key.field_name in self._definition.get('data', {}) return key.field_name in self._fields
else:
return False
def get_data(self): # would like to just take a key, but there's a bunch of magic in DbModel for constructing the key via
# a private method
def field_value_provenance(self, key_scope, key_name):
""" """
Intended only for use by persistence layer to get the native definition['data'] rep Where the field value comes from: one of [PROVENANCE_LOCAL, PROVENANCE_DEFAULT, PROVENANCE_INHERITED].
""" """
if isinstance(self._definition, DefinitionLazyLoader): # handle any special cases
self._definition = self._definition.fetch() if key_scope == Scope.content:
return self._definition.get('data') if key_name == 'location':
return PROVENANCE_LOCAL
elif key_name == 'category':
return PROVENANCE_LOCAL
else:
self._load_definition()
if key_name in self._fields:
return PROVENANCE_LOCAL
else:
return PROVENANCE_DEFAULT
elif key_scope == Scope.parent:
return PROVENANCE_DEFAULT
elif key_name in self._fields:
return PROVENANCE_LOCAL
elif key_scope == Scope.settings and key_name in self._inherited_settings:
return PROVENANCE_INHERITED
else:
return PROVENANCE_DEFAULT
def get_own_metadata(self): def get_inherited_settings(self):
""" """
Get the metadata explicitly set on this element. Get the metadata set by the ancestors (which own metadata may override or not)
""" """
return self._metadata return self._inherited_settings
def get_inherited_metadata(self): def _load_definition(self):
""" """
Get the metadata set by the ancestors (which own metadata may override or not) Update fields w/ the lazily loaded definitions
""" """
return self._inherited_metadata if isinstance(self._definition, DefinitionLazyLoader):
persisted_definition = self._definition.fetch()
if persisted_definition is not None:
self._fields.update(persisted_definition.get('fields'))
# do we want to cache any of the edit_info?
self._definition = None # already loaded
...@@ -16,8 +16,7 @@ class PersistentCourseFactory(factory.Factory): ...@@ -16,8 +16,7 @@ class PersistentCourseFactory(factory.Factory):
* prettyid: defaults to 999 * prettyid: defaults to 999
* display_name * display_name
* user_id * user_id
* data (optional) the data payload to save in the course item * fields (optional) the settings and content payloads. If display_name is in the metadata, that takes
* metadata (optional) the metadata payload. If display_name is in the metadata, that takes
precedence over any display_name provided directly. precedence over any display_name provided directly.
""" """
FACTORY_FOR = CourseDescriptor FACTORY_FOR = CourseDescriptor
...@@ -28,7 +27,7 @@ class PersistentCourseFactory(factory.Factory): ...@@ -28,7 +27,7 @@ class PersistentCourseFactory(factory.Factory):
user_id = "test_user" user_id = "test_user"
data = None data = None
metadata = None metadata = None
master_version = 'draft' master_branch = 'draft'
# pylint: disable=W0613 # pylint: disable=W0613
@classmethod @classmethod
...@@ -38,17 +37,14 @@ class PersistentCourseFactory(factory.Factory): ...@@ -38,17 +37,14 @@ class PersistentCourseFactory(factory.Factory):
prettyid = kwargs.get('prettyid') prettyid = kwargs.get('prettyid')
display_name = kwargs.get('display_name') display_name = kwargs.get('display_name')
user_id = kwargs.get('user_id') user_id = kwargs.get('user_id')
data = kwargs.get('data') fields = kwargs.get('fields', {})
metadata = kwargs.get('metadata', {}) if display_name and 'display_name' not in fields:
if metadata is None: fields['display_name'] = display_name
metadata = {}
if 'display_name' not in metadata:
metadata['display_name'] = display_name
# Write the data to the mongo datastore # Write the data to the mongo datastore
new_course = modulestore('split').create_course( new_course = modulestore('split').create_course(
org, prettyid, user_id, metadata=metadata, course_data=data, id_root=prettyid, org, prettyid, user_id, fields=fields, id_root=prettyid,
master_version=kwargs.get('master_version')) master_branch=kwargs.get('master_branch'))
return new_course return new_course
...@@ -70,26 +66,23 @@ class ItemFactory(factory.Factory): ...@@ -70,26 +66,23 @@ class ItemFactory(factory.Factory):
""" """
Uses *kwargs*: Uses *kwargs*:
*parent_location* (required): the location of the course & possibly parent :param parent_location: (required) the location of the course & possibly parent
*category* (defaults to 'chapter') :param category: (defaults to 'chapter')
*data* (optional): the data for the item :param fields: (optional) the data for the item
definition_locator (optional): the DescriptorLocator for the definition this uses or branches :param definition_locator (optional): the DescriptorLocator for the definition this uses or branches
*display_name* (optional): the display name of the item :param display_name (optional): the display name of the item
*metadata* (optional): dictionary of metadata attributes (display_name here takes
precedence over the above attr)
""" """
metadata = kwargs.get('metadata', {}) fields = kwargs.get('fields', {})
if 'display_name' not in metadata and 'display_name' in kwargs: if 'display_name' not in fields and 'display_name' in kwargs:
metadata['display_name'] = kwargs['display_name'] fields['display_name'] = kwargs['display_name']
return modulestore('split').create_item(kwargs['parent_location'], kwargs['category'], return modulestore('split').create_item(kwargs['parent_location'], kwargs['category'],
kwargs['user_id'], definition_locator=kwargs.get('definition_locator'), kwargs['user_id'], definition_locator=kwargs.get('definition_locator'),
new_def_data=kwargs.get('data'), metadata=metadata) fields=fields)
@classmethod @classmethod
def _build(cls, target_class, *args, **kwargs): def _build(cls, target_class, *args, **kwargs):
......
...@@ -187,6 +187,7 @@ class SplitModuleCourseTests(SplitModuleTest): ...@@ -187,6 +187,7 @@ class SplitModuleCourseTests(SplitModuleTest):
self.assertEqual(course.category, 'course') self.assertEqual(course.category, 'course')
self.assertEqual(len(course.tabs), 6) self.assertEqual(len(course.tabs), 6)
self.assertEqual(course.display_name, "The Ancient Greek Hero") self.assertEqual(course.display_name, "The Ancient Greek Hero")
self.assertEqual(course.lms.graceperiod, datetime.timedelta(hours=2))
self.assertIsNone(course.advertised_start) self.assertIsNone(course.advertised_start)
self.assertEqual(len(course.children), 0) self.assertEqual(len(course.children), 0)
self.assertEqual(course.definition_locator.definition_id, "head12345_11") self.assertEqual(course.definition_locator.definition_id, "head12345_11")
...@@ -438,12 +439,12 @@ class SplitModuleItemTests(SplitModuleTest): ...@@ -438,12 +439,12 @@ class SplitModuleItemTests(SplitModuleTest):
qualifiers= qualifiers=
{ {
'category': 'chapter', 'category': 'chapter',
'metadata': {'display_name': {'$regex': 'Hera'}} 'fields': {'display_name': {'$regex': 'Hera'}}
} }
) )
self.assertEqual(len(matches), 2) self.assertEqual(len(matches), 2)
matches = modulestore().get_items(locator, qualifiers={'children': 'chapter2'}) matches = modulestore().get_items(locator, qualifiers={'fields': {'children': 'chapter2'}})
self.assertEqual(len(matches), 1) self.assertEqual(len(matches), 1)
self.assertEqual(matches[0].location.usage_id, 'head12345') self.assertEqual(matches[0].location.usage_id, 'head12345')
...@@ -507,8 +508,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -507,8 +508,7 @@ class TestItemCrud(SplitModuleTest):
def test_create_minimal_item(self): def test_create_minimal_item(self):
""" """
create_item(course_or_parent_locator, category, user, definition_locator=None, new_def_data=None, create_item(course_or_parent_locator, category, user, definition_locator=None, fields): new_desciptor
metadata=None): new_desciptor
""" """
# grab link to course to ensure new versioning works # grab link to course to ensure new versioning works
locator = CourseLocator(course_id="GreekHero", branch='draft') locator = CourseLocator(course_id="GreekHero", branch='draft')
...@@ -518,7 +518,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -518,7 +518,7 @@ class TestItemCrud(SplitModuleTest):
category = 'sequential' category = 'sequential'
new_module = modulestore().create_item( new_module = modulestore().create_item(
locator, category, 'user123', locator, category, 'user123',
metadata={'display_name': 'new sequential'} fields={'display_name': 'new sequential'}
) )
# check that course version changed and course's previous is the other one # check that course version changed and course's previous is the other one
self.assertEqual(new_module.location.course_id, "GreekHero") self.assertEqual(new_module.location.course_id, "GreekHero")
...@@ -553,7 +553,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -553,7 +553,7 @@ class TestItemCrud(SplitModuleTest):
category = 'chapter' category = 'chapter'
new_module = modulestore().create_item( new_module = modulestore().create_item(
locator, category, 'user123', locator, category, 'user123',
metadata={'display_name': 'new chapter'}, fields={'display_name': 'new chapter'},
definition_locator=DescriptionLocator("chapter12345_2") definition_locator=DescriptionLocator("chapter12345_2")
) )
# check that course version changed and course's previous is the other one # check that course version changed and course's previous is the other one
...@@ -574,15 +574,13 @@ class TestItemCrud(SplitModuleTest): ...@@ -574,15 +574,13 @@ class TestItemCrud(SplitModuleTest):
new_payload = "<problem>empty</problem>" new_payload = "<problem>empty</problem>"
new_module = modulestore().create_item( new_module = modulestore().create_item(
locator, category, 'anotheruser', locator, category, 'anotheruser',
metadata={'display_name': 'problem 1'}, fields={'display_name': 'problem 1', 'data': new_payload},
new_def_data=new_payload
) )
another_payload = "<problem>not empty</problem>" another_payload = "<problem>not empty</problem>"
another_module = modulestore().create_item( another_module = modulestore().create_item(
locator, category, 'anotheruser', locator, category, 'anotheruser',
metadata={'display_name': 'problem 2'}, fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=DescriptionLocator("problem12345_3_1"), definition_locator=DescriptionLocator("problem12345_3_1"),
new_def_data=another_payload
) )
# check that course version changed and course's previous is the other one # check that course version changed and course's previous is the other one
parent = modulestore().get_item(locator) parent = modulestore().get_item(locator)
...@@ -616,6 +614,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -616,6 +614,7 @@ class TestItemCrud(SplitModuleTest):
self.assertNotEqual(problem.max_attempts, 4, "Invalidates rest of test") self.assertNotEqual(problem.max_attempts, 4, "Invalidates rest of test")
problem.max_attempts = 4 problem.max_attempts = 4
problem.save() # decache above setting into the kvs
updated_problem = modulestore().update_item(problem, 'changeMaven') updated_problem = modulestore().update_item(problem, 'changeMaven')
# check that course version changed and course's previous is the other one # check that course version changed and course's previous is the other one
self.assertEqual(updated_problem.definition_locator.definition_id, pre_def_id) self.assertEqual(updated_problem.definition_locator.definition_id, pre_def_id)
...@@ -651,6 +650,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -651,6 +650,7 @@ class TestItemCrud(SplitModuleTest):
# reorder children # reorder children
self.assertGreater(len(block.children), 0, "meaningless test") self.assertGreater(len(block.children), 0, "meaningless test")
moved_child = block.children.pop() moved_child = block.children.pop()
block.save() # decache model changes
updated_problem = modulestore().update_item(block, 'childchanger') updated_problem = modulestore().update_item(block, 'childchanger')
# check that course version changed and course's previous is the other one # check that course version changed and course's previous is the other one
self.assertEqual(updated_problem.definition_locator.definition_id, pre_def_id) self.assertEqual(updated_problem.definition_locator.definition_id, pre_def_id)
...@@ -660,6 +660,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -660,6 +660,7 @@ class TestItemCrud(SplitModuleTest):
locator.usage_id = "chapter1" locator.usage_id = "chapter1"
other_block = modulestore().get_item(locator) other_block = modulestore().get_item(locator)
other_block.children.append(moved_child) other_block.children.append(moved_child)
other_block.save() # decache model changes
other_updated = modulestore().update_item(other_block, 'childchanger') other_updated = modulestore().update_item(other_block, 'childchanger')
self.assertIn(moved_child, other_updated.children) self.assertIn(moved_child, other_updated.children)
...@@ -673,6 +674,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -673,6 +674,7 @@ class TestItemCrud(SplitModuleTest):
pre_version_guid = block.location.version_guid pre_version_guid = block.location.version_guid
block.grading_policy['GRADER'][0]['min_count'] = 13 block.grading_policy['GRADER'][0]['min_count'] = 13
block.save() # decache model changes
updated_block = modulestore().update_item(block, 'definition_changer') updated_block = modulestore().update_item(block, 'definition_changer')
self.assertNotEqual(updated_block.definition_locator.definition_id, pre_def_id) self.assertNotEqual(updated_block.definition_locator.definition_id, pre_def_id)
...@@ -689,15 +691,13 @@ class TestItemCrud(SplitModuleTest): ...@@ -689,15 +691,13 @@ class TestItemCrud(SplitModuleTest):
new_payload = "<problem>empty</problem>" new_payload = "<problem>empty</problem>"
modulestore().create_item( modulestore().create_item(
locator, category, 'test_update_manifold', locator, category, 'test_update_manifold',
metadata={'display_name': 'problem 1'}, fields={'display_name': 'problem 1', 'data': new_payload},
new_def_data=new_payload
) )
another_payload = "<problem>not empty</problem>" another_payload = "<problem>not empty</problem>"
modulestore().create_item( modulestore().create_item(
locator, category, 'test_update_manifold', locator, category, 'test_update_manifold',
metadata={'display_name': 'problem 2'}, fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=DescriptionLocator("problem12345_3_1"), definition_locator=DescriptionLocator("problem12345_3_1"),
new_def_data=another_payload
) )
# pylint: disable=W0212 # pylint: disable=W0212
modulestore()._clear_cache() modulestore()._clear_cache()
...@@ -712,6 +712,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -712,6 +712,7 @@ class TestItemCrud(SplitModuleTest):
block.children = block.children[1:] + [block.children[0]] block.children = block.children[1:] + [block.children[0]]
block.advertised_start = "Soon" block.advertised_start = "Soon"
block.save() # decache model changes
updated_block = modulestore().update_item(block, "test_update_manifold") updated_block = modulestore().update_item(block, "test_update_manifold")
self.assertNotEqual(updated_block.definition_locator.definition_id, pre_def_id) self.assertNotEqual(updated_block.definition_locator.definition_id, pre_def_id)
self.assertNotEqual(updated_block.location.version_guid, pre_version_guid) self.assertNotEqual(updated_block.location.version_guid, pre_version_guid)
...@@ -733,7 +734,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -733,7 +734,7 @@ class TestItemCrud(SplitModuleTest):
# delete a leaf # delete a leaf
problems = modulestore().get_items(reusable_location, {'category': 'problem'}) problems = modulestore().get_items(reusable_location, {'category': 'problem'})
locn_to_del = problems[0].location locn_to_del = problems[0].location
new_course_loc = modulestore().delete_item(locn_to_del, 'deleting_user') new_course_loc = modulestore().delete_item(locn_to_del, 'deleting_user', delete_children=True)
deleted = BlockUsageLocator(course_id=reusable_location.course_id, deleted = BlockUsageLocator(course_id=reusable_location.course_id,
branch=reusable_location.branch, branch=reusable_location.branch,
usage_id=locn_to_del.usage_id) usage_id=locn_to_del.usage_id)
...@@ -748,7 +749,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -748,7 +749,7 @@ class TestItemCrud(SplitModuleTest):
# delete a subtree # delete a subtree
nodes = modulestore().get_items(reusable_location, {'category': 'chapter'}) nodes = modulestore().get_items(reusable_location, {'category': 'chapter'})
new_course_loc = modulestore().delete_item(nodes[0].location, 'deleting_user') new_course_loc = modulestore().delete_item(nodes[0].location, 'deleting_user', delete_children=True)
# check subtree # check subtree
def check_subtree(node): def check_subtree(node):
...@@ -855,7 +856,7 @@ class TestCourseCreation(SplitModuleTest): ...@@ -855,7 +856,7 @@ class TestCourseCreation(SplitModuleTest):
# using new_draft.location will insert the chapter under the course root # using new_draft.location will insert the chapter under the course root
new_item = modulestore().create_item( new_item = modulestore().create_item(
new_draft.location, 'chapter', 'leech_master', new_draft.location, 'chapter', 'leech_master',
metadata={'display_name': 'new chapter'} fields={'display_name': 'new chapter'}
) )
new_draft_locator.version_guid = None new_draft_locator.version_guid = None
new_index = modulestore().get_course_index_info(new_draft_locator) new_index = modulestore().get_course_index_info(new_draft_locator)
...@@ -887,20 +888,18 @@ class TestCourseCreation(SplitModuleTest): ...@@ -887,20 +888,18 @@ class TestCourseCreation(SplitModuleTest):
original_locator = CourseLocator(course_id="contender", branch='draft') original_locator = CourseLocator(course_id="contender", branch='draft')
original = modulestore().get_course(original_locator) original = modulestore().get_course(original_locator)
original_index = modulestore().get_course_index_info(original_locator) original_index = modulestore().get_course_index_info(original_locator)
data_payload = {} fields = {}
metadata_payload = {}
for field in original.fields: for field in original.fields:
if field.scope == Scope.content and field.name != 'location': if field.scope == Scope.content and field.name != 'location':
data_payload[field.name] = getattr(original, field.name) fields[field.name] = getattr(original, field.name)
elif field.scope == Scope.settings: elif field.scope == Scope.settings:
metadata_payload[field.name] = getattr(original, field.name) fields[field.name] = getattr(original, field.name)
data_payload['grading_policy']['GRADE_CUTOFFS'] = {'A': .9, 'B': .8, 'C': .65} fields['grading_policy']['GRADE_CUTOFFS'] = {'A': .9, 'B': .8, 'C': .65}
metadata_payload['display_name'] = 'Derivative' fields['display_name'] = 'Derivative'
new_draft = modulestore().create_course( new_draft = modulestore().create_course(
'leech', 'derivative', 'leech_master', id_root='counter', 'leech', 'derivative', 'leech_master', id_root='counter',
versions_dict={'draft': original_index['versions']['draft']}, versions_dict={'draft': original_index['versions']['draft']},
course_data=data_payload, fields=fields
metadata=metadata_payload
) )
new_draft_locator = new_draft.location new_draft_locator = new_draft.location
self.assertRegexpMatches(new_draft_locator.course_id, r'counter.*') self.assertRegexpMatches(new_draft_locator.course_id, r'counter.*')
...@@ -913,10 +912,10 @@ class TestCourseCreation(SplitModuleTest): ...@@ -913,10 +912,10 @@ class TestCourseCreation(SplitModuleTest):
self.assertGreaterEqual(new_index["edited_on"], pre_time) self.assertGreaterEqual(new_index["edited_on"], pre_time)
self.assertLessEqual(new_index["edited_on"], datetime.datetime.now(UTC)) self.assertLessEqual(new_index["edited_on"], datetime.datetime.now(UTC))
self.assertEqual(new_index['edited_by'], 'leech_master') self.assertEqual(new_index['edited_by'], 'leech_master')
self.assertEqual(new_draft.display_name, metadata_payload['display_name']) self.assertEqual(new_draft.display_name, fields['display_name'])
self.assertDictEqual( self.assertDictEqual(
new_draft.grading_policy['GRADE_CUTOFFS'], new_draft.grading_policy['GRADE_CUTOFFS'],
data_payload['grading_policy']['GRADE_CUTOFFS'] fields['grading_policy']['GRADE_CUTOFFS']
) )
def test_update_course_index(self): def test_update_course_index(self):
......
...@@ -587,33 +587,20 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -587,33 +587,20 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
Creates an instance of this descriptor from the supplied json_data. Creates an instance of this descriptor from the supplied json_data.
This may be overridden by subclasses This may be overridden by subclasses
json_data: A json object with the keys 'definition' and 'metadata',
definition: A json object with the keys 'data' and 'children'
data: A json value
children: A list of edX Location urls
metadata: A json object with any keys
This json_data is transformed to model_data using the following rules:
1) The model data contains all of the fields from metadata
2) The model data contains the 'children' array
3) If 'definition.data' is a json object, model data contains all of its fields
Otherwise, it contains the single field 'data'
4) Any value later in this list overrides a value earlier in this list
json_data: json_data:
- 'category': the xmodule category (required) - 'category': the xmodule category (required)
- 'metadata': a dict of locally set metadata (not inherited) - 'fields': a dict of locally set fields (not inherited)
- 'children': a list of children's usage_ids w/in this course - 'definition': (optional) the db id for the definition record (not the definition content) or a
- 'definition': definitionLazyLoader
- '_id' (optional): the usage_id of this. Will generate one if not given one. - '_id' (optional): the usage_id of this. Will generate one if not given one.
""" """
usage_id = json_data.get('_id', None) usage_id = json_data.get('_id', None)
if not '_inherited_metadata' in json_data and parent_xblock is not None: if not '_inherited_settings' in json_data and parent_xblock is not None:
json_data['_inherited_metadata'] = parent_xblock.xblock_kvs.get_inherited_metadata().copy() json_data['_inherited_settings'] = parent_xblock.xblock_kvs.get_inherited_settings().copy()
json_metadata = json_data.get('metadata', {}) json_fields = json_data.get('fields', {})
for field in inheritance.INHERITABLE_METADATA: for field in inheritance.INHERITABLE_METADATA:
if field in json_metadata: if field in json_fields:
json_data['_inherited_metadata'][field] = json_metadata[field] json_data['_inherited_settings'][field] = json_fields[field]
new_block = system.xblock_from_json(cls, usage_id, json_data) new_block = system.xblock_from_json(cls, usage_id, json_data)
if parent_xblock is not None: if parent_xblock is not None:
...@@ -736,6 +723,27 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -736,6 +723,27 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# We are not allowing editing of xblock tag and name fields at this time (for any component). # We are not allowing editing of xblock tag and name fields at this time (for any component).
return [XBlock.tags, XBlock.name] return [XBlock.tags, XBlock.name]
def get_set_fields_by_scope(self, scope=Scope.content):
"""
Get a dictionary of the fields for the given scope which are set explicitly on this xblock. (Including
any set to None.)
"""
if scope == Scope.settings and hasattr(self, '_inherited_metadata'):
inherited_metadata = getattr(self, '_inherited_metadata')
result = {}
for field in self.fields:
if (field.scope == scope and
field.name in self._model_data and
field.name not in inherited_metadata):
result[field.name] = getattr(self, field.name)
return result
else:
result = {}
for field in self.fields:
if (field.scope == scope and field.name in self._model_data):
result[field.name] = getattr(self, field.name)
return result
@property @property
def editable_metadata_fields(self): def editable_metadata_fields(self):
""" """
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
{ {
"_id":"head12345_12", "_id":"head12345_12",
"category":"course", "category":"course",
"data":{ "fields":{
"textbooks":[ "textbooks":[
], ],
...@@ -43,15 +43,17 @@ ...@@ -43,15 +43,17 @@
}, },
"wiki_slug":null "wiki_slug":null
}, },
"edit_info": {
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364481713238}, "edited_on":{"$date" : 1364481713238},
"previous_version":"head12345_11", "previous_version":"head12345_11",
"original_version":"head12345_10" "original_version":"head12345_10"
}
}, },
{ {
"_id":"head12345_11", "_id":"head12345_11",
"category":"course", "category":"course",
"data":{ "fields":{
"textbooks":[ "textbooks":[
], ],
...@@ -92,15 +94,17 @@ ...@@ -92,15 +94,17 @@
}, },
"wiki_slug":null "wiki_slug":null
}, },
"edit_info": {
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364481713238}, "edited_on":{"$date" : 1364481713238},
"previous_version":"head12345_10", "previous_version":"head12345_10",
"original_version":"head12345_10" "original_version":"head12345_10"
}
}, },
{ {
"_id":"head12345_10", "_id":"head12345_10",
"category":"course", "category":"course",
"data":{ "fields":{
"textbooks":[ "textbooks":[
], ],
...@@ -141,15 +145,17 @@ ...@@ -141,15 +145,17 @@
}, },
"wiki_slug":null "wiki_slug":null
}, },
"edit_info": {
"edited_by":"test@edx.org", "edited_by":"test@edx.org",
"edited_on":{"$date": 1364473713238}, "edited_on":{"$date": 1364473713238},
"previous_version":null, "previous_version":null,
"original_version":"head12345_10" "original_version":"head12345_10"
}
}, },
{ {
"_id":"head23456_1", "_id":"head23456_1",
"category":"course", "category":"course",
"data":{ "fields":{
"textbooks":[ "textbooks":[
], ],
...@@ -190,15 +196,17 @@ ...@@ -190,15 +196,17 @@
}, },
"wiki_slug":null "wiki_slug":null
}, },
"edit_info": {
"edited_by":"test@edx.org", "edited_by":"test@edx.org",
"edited_on":{"$date": 1364481313238}, "edited_on":{"$date": 1364481313238},
"previous_version":"head23456_0", "previous_version":"head23456_0",
"original_version":"head23456_0" "original_version":"head23456_0"
}
}, },
{ {
"_id":"head23456_0", "_id":"head23456_0",
"category":"course", "category":"course",
"data":{ "fields":{
"textbooks":[ "textbooks":[
], ],
...@@ -239,15 +247,17 @@ ...@@ -239,15 +247,17 @@
}, },
"wiki_slug":null "wiki_slug":null
}, },
"edit_info": {
"edited_by":"test@edx.org", "edited_by":"test@edx.org",
"edited_on":{"$date" : 1364481313238}, "edited_on":{"$date" : 1364481313238},
"previous_version":null, "previous_version":null,
"original_version":"head23456_0" "original_version":"head23456_0"
}
}, },
{ {
"_id":"head345679_1", "_id":"head345679_1",
"category":"course", "category":"course",
"data":{ "fields":{
"textbooks":[ "textbooks":[
], ],
...@@ -281,54 +291,66 @@ ...@@ -281,54 +291,66 @@
}, },
"wiki_slug":null "wiki_slug":null
}, },
"edit_info": {
"edited_by":"test@edx.org", "edited_by":"test@edx.org",
"edited_on":{"$date" : 1364481313238}, "edited_on":{"$date" : 1364481313238},
"previous_version":null, "previous_version":null,
"original_version":"head23456_0" "original_version":"head23456_0"
}
}, },
{ {
"_id":"chapter12345_1", "_id":"chapter12345_1",
"category":"chapter", "category":"chapter",
"data":null, "fields":{},
"edit_info": {
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238}, "edited_on":{"$date" : 1364483713238},
"previous_version":null, "previous_version":null,
"original_version":"chapter12345_1" "original_version":"chapter12345_1"
}
}, },
{ {
"_id":"chapter12345_2", "_id":"chapter12345_2",
"category":"chapter", "category":"chapter",
"data":null, "fields":{},
"edit_info": {
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238}, "edited_on":{"$date" : 1364483713238},
"previous_version":null, "previous_version":null,
"original_version":"chapter12345_2" "original_version":"chapter12345_2"
}
}, },
{ {
"_id":"chapter12345_3", "_id":"chapter12345_3",
"category":"chapter", "category":"chapter",
"data":null, "fields":{},
"edit_info": {
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238}, "edited_on":{"$date" : 1364483713238},
"previous_version":null, "previous_version":null,
"original_version":"chapter12345_3" "original_version":"chapter12345_3"
}
}, },
{ {
"_id":"problem12345_3_1", "_id":"problem12345_3_1",
"category":"problem", "category":"problem",
"data":"", "fields": {"data": ""},
"edit_info": {
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238}, "edited_on":{"$date" : 1364483713238},
"previous_version":null, "previous_version":null,
"original_version":"problem12345_3_1" "original_version":"problem12345_3_1"
}
}, },
{ {
"_id":"problem12345_3_2", "_id":"problem12345_3_2",
"category":"problem", "category":"problem",
"data":"", "fields": {"data": ""},
"edit_info": {
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238}, "edited_on":{"$date" : 1364483713238},
"previous_version":null, "previous_version":null,
"original_version":"problem12345_3_2" "original_version":"problem12345_3_2"
} }
}
] ]
\ No newline at end of file
...@@ -10,14 +10,14 @@ ...@@ -10,14 +10,14 @@
}, },
"blocks":{ "blocks":{
"head12345":{ "head12345":{
"category":"course",
"definition":"head12345_12",
"fields":{
"children":[ "children":[
"chapter1", "chapter1",
"chapter2", "chapter2",
"chapter3" "chapter3"
], ],
"category":"course",
"definition":"head12345_12",
"metadata":{
"end":"2013-06-13T04:30", "end":"2013-06-13T04:30",
"tabs":[ "tabs":[
{ {
...@@ -54,88 +54,99 @@ ...@@ -54,88 +54,99 @@
"advertised_start":"Fall 2013", "advertised_start":"Fall 2013",
"display_name":"The Ancient Greek Hero" "display_name":"The Ancient Greek Hero"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd0000" }, "update_version":{ "$oid" : "1d00000000000000dddd0000" },
"previous_version":{ "$oid" : "1d00000000000000dddd1111" }, "previous_version":{ "$oid" : "1d00000000000000dddd1111" },
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{ "edited_on":{
"$date":1364483713238 "$date":1364483713238
} }
}
}, },
"chapter1":{ "chapter1":{
"category":"chapter",
"definition":"chapter12345_1",
"fields":{
"children":[ "children":[
], ],
"category":"chapter",
"definition":"chapter12345_1",
"metadata":{
"display_name":"Hercules" "display_name":"Hercules"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd0000" }, "update_version":{ "$oid" : "1d00000000000000dddd0000" },
"previous_version":null, "previous_version":null,
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{ "edited_on":{
"$date":1364483713238 "$date":1364483713238
} }
}
}, },
"chapter2":{ "chapter2":{
"category":"chapter",
"definition":"chapter12345_2",
"fields":{
"children":[ "children":[
], ],
"category":"chapter",
"definition":"chapter12345_2",
"metadata":{
"display_name":"Hera heckles Hercules" "display_name":"Hera heckles Hercules"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd0000" }, "update_version":{ "$oid" : "1d00000000000000dddd0000" },
"previous_version":null, "previous_version":null,
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{ "edited_on":{
"$date":1364483713238 "$date":1364483713238
} }
}
}, },
"chapter3":{ "chapter3":{
"category":"chapter",
"definition":"chapter12345_3",
"fields":{
"children":[ "children":[
"problem1", "problem1",
"problem3_2" "problem3_2"
], ],
"category":"chapter",
"definition":"chapter12345_3",
"metadata":{
"display_name":"Hera cuckolds Zeus" "display_name":"Hera cuckolds Zeus"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd0000" }, "update_version":{ "$oid" : "1d00000000000000dddd0000" },
"previous_version":null, "previous_version":null,
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{ "edited_on":{
"$date":1364483713238 "$date":1364483713238
} }
}
}, },
"problem1":{ "problem1":{
"category":"problem",
"definition":"problem12345_3_1",
"fields":{
"children":[ "children":[
], ],
"category":"problem",
"definition":"problem12345_3_1",
"metadata":{
"display_name":"Problem 3.1", "display_name":"Problem 3.1",
"graceperiod":"4 hours 0 minutes 0 seconds" "graceperiod":"4 hours 0 minutes 0 seconds"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd0000" }, "update_version":{ "$oid" : "1d00000000000000dddd0000" },
"previous_version":null, "previous_version":null,
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
"edited_on":{ "edited_on":{
"$date":1364483713238 "$date":1364483713238
} }
}
}, },
"problem3_2":{ "problem3_2":{
"category":"problem",
"definition":"problem12345_3_2",
"fields":{
"children":[ "children":[
], ],
"category":"problem",
"definition":"problem12345_3_2",
"metadata":{
"display_name":"Problem 3.2" "display_name":"Problem 3.2"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd0000" }, "update_version":{ "$oid" : "1d00000000000000dddd0000" },
"previous_version":null, "previous_version":null,
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
...@@ -144,6 +155,7 @@ ...@@ -144,6 +155,7 @@
} }
} }
} }
}
}, },
{ {
"_id": { "$oid" : "1d00000000000000dddd1111"}, "_id": { "$oid" : "1d00000000000000dddd1111"},
...@@ -156,12 +168,12 @@ ...@@ -156,12 +168,12 @@
}, },
"blocks":{ "blocks":{
"head12345":{ "head12345":{
"category":"course",
"definition":"head12345_11",
"fields":{
"children":[ "children":[
], ],
"category":"course",
"definition":"head12345_11",
"metadata":{
"end":"2013-04-13T04:30", "end":"2013-04-13T04:30",
"tabs":[ "tabs":[
{ {
...@@ -198,6 +210,7 @@ ...@@ -198,6 +210,7 @@
"advertised_start":null, "advertised_start":null,
"display_name":"The Ancient Greek Hero" "display_name":"The Ancient Greek Hero"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd1111" }, "update_version":{ "$oid" : "1d00000000000000dddd1111" },
"previous_version":{ "$oid" : "1d00000000000000dddd3333" }, "previous_version":{ "$oid" : "1d00000000000000dddd3333" },
"edited_by":"testassist@edx.org", "edited_by":"testassist@edx.org",
...@@ -206,6 +219,7 @@ ...@@ -206,6 +219,7 @@
} }
} }
} }
}
}, },
{ {
"_id": { "$oid" : "1d00000000000000dddd3333"}, "_id": { "$oid" : "1d00000000000000dddd3333"},
...@@ -218,12 +232,12 @@ ...@@ -218,12 +232,12 @@
}, },
"blocks":{ "blocks":{
"head12345":{ "head12345":{
"category":"course",
"definition":"head12345_10",
"fields":{
"children":[ "children":[
], ],
"category":"course",
"definition":"head12345_10",
"metadata":{
"end":null, "end":null,
"tabs":[ "tabs":[
{ {
...@@ -250,6 +264,7 @@ ...@@ -250,6 +264,7 @@
"advertised_start":null, "advertised_start":null,
"display_name":"The Ancient Greek Hero" "display_name":"The Ancient Greek Hero"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd3333" }, "update_version":{ "$oid" : "1d00000000000000dddd3333" },
"previous_version":null, "previous_version":null,
"edited_by":"test@edx.org", "edited_by":"test@edx.org",
...@@ -258,6 +273,7 @@ ...@@ -258,6 +273,7 @@
} }
} }
} }
}
}, },
{ {
"_id": { "$oid" : "1d00000000000000dddd2222"}, "_id": { "$oid" : "1d00000000000000dddd2222"},
...@@ -270,12 +286,12 @@ ...@@ -270,12 +286,12 @@
}, },
"blocks":{ "blocks":{
"head23456":{ "head23456":{
"category":"course",
"definition":"head23456_1",
"fields":{
"children":[ "children":[
], ],
"category":"course",
"definition":"head23456_1",
"metadata":{
"end":null, "end":null,
"tabs":[ "tabs":[
{ {
...@@ -302,12 +318,14 @@ ...@@ -302,12 +318,14 @@
"advertised_start":null, "advertised_start":null,
"display_name":"The most wonderful course" "display_name":"The most wonderful course"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd2222" }, "update_version":{ "$oid" : "1d00000000000000dddd2222" },
"previous_version":{ "$oid" : "1d00000000000000dddd4444" }, "previous_version":{ "$oid" : "1d00000000000000dddd4444" },
"edited_by":"test@edx.org", "edited_by":"test@edx.org",
"edited_on":{ "edited_on":{
"$date":1364481313238 "$date":1364481313238
} }
}
} }
} }
...@@ -323,12 +341,12 @@ ...@@ -323,12 +341,12 @@
}, },
"blocks":{ "blocks":{
"head23456":{ "head23456":{
"category":"course",
"definition":"head23456_0",
"fields":{
"children":[ "children":[
], ],
"category":"course",
"definition":"head23456_0",
"metadata":{
"end":null, "end":null,
"tabs":[ "tabs":[
{ {
...@@ -355,6 +373,7 @@ ...@@ -355,6 +373,7 @@
"advertised_start":null, "advertised_start":null,
"display_name":"A wonderful course" "display_name":"A wonderful course"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd4444" }, "update_version":{ "$oid" : "1d00000000000000dddd4444" },
"previous_version":null, "previous_version":null,
"edited_by":"test@edx.org", "edited_by":"test@edx.org",
...@@ -363,6 +382,7 @@ ...@@ -363,6 +382,7 @@
} }
} }
} }
}
}, },
{ {
"_id": { "$oid" : "1d00000000000000eeee0000"}, "_id": { "$oid" : "1d00000000000000eeee0000"},
...@@ -375,12 +395,12 @@ ...@@ -375,12 +395,12 @@
}, },
"blocks":{ "blocks":{
"head23456":{ "head23456":{
"category":"course",
"definition":"head23456_1",
"fields":{
"children":[ "children":[
], ],
"category":"course",
"definition":"head23456_1",
"metadata":{
"end":null, "end":null,
"tabs":[ "tabs":[
{ {
...@@ -407,6 +427,7 @@ ...@@ -407,6 +427,7 @@
"advertised_start":null, "advertised_start":null,
"display_name":"The most wonderful course" "display_name":"The most wonderful course"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000eeee0000" }, "update_version":{ "$oid" : "1d00000000000000eeee0000" },
"previous_version":null, "previous_version":null,
"edited_by":"test@edx.org", "edited_by":"test@edx.org",
...@@ -415,6 +436,7 @@ ...@@ -415,6 +436,7 @@
} }
} }
} }
}
}, },
{ {
"_id": { "$oid" : "1d00000000000000dddd5555"}, "_id": { "$oid" : "1d00000000000000dddd5555"},
...@@ -427,12 +449,12 @@ ...@@ -427,12 +449,12 @@
}, },
"blocks":{ "blocks":{
"head345679":{ "head345679":{
"category":"course",
"definition":"head345679_1",
"fields":{
"children":[ "children":[
], ],
"category":"course",
"definition":"head345679_1",
"metadata":{
"end":null, "end":null,
"tabs":[ "tabs":[
{ {
...@@ -459,6 +481,7 @@ ...@@ -459,6 +481,7 @@
"advertised_start":null, "advertised_start":null,
"display_name":"Yet another contender" "display_name":"Yet another contender"
}, },
"edit_info": {
"update_version":{ "$oid" : "1d00000000000000dddd5555" }, "update_version":{ "$oid" : "1d00000000000000dddd5555" },
"previous_version":null, "previous_version":null,
"edited_by":"test@guestx.edu", "edited_by":"test@guestx.edu",
...@@ -468,4 +491,5 @@ ...@@ -468,4 +491,5 @@
} }
} }
} }
}
] ]
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