diff --git a/cms/djangoapps/contentstore/tests/test_crud.py b/cms/djangoapps/contentstore/tests/test_crud.py
index e12711a6ff..5beef20d6c 100644
--- a/cms/djangoapps/contentstore/tests/test_crud.py
+++ b/cms/djangoapps/contentstore/tests/test_crud.py
@@ -75,7 +75,7 @@ class TemplateTests(unittest.TestCase):
display_name='fun test course', user_id='testbot')
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)
self.assertIsInstance(test_chapter, SequenceDescriptor)
self.assertEqual(test_chapter.display_name, 'chapter n')
@@ -84,7 +84,7 @@ class TemplateTests(unittest.TestCase):
# test w/ a definition (e.g., a problem)
test_def_content = 'boo'
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)
self.assertIsInstance(test_problem, CapaDescriptor)
self.assertEqual(test_problem.data, test_def_content)
@@ -99,11 +99,12 @@ class TemplateTests(unittest.TestCase):
test_course = persistent_factories.PersistentCourseFactory.create(org='testx', prettyid='tempcourse',
display_name='fun test course', user_id='testbot')
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_def_content = 'boo'
- test_problem = XModuleDescriptor.load_from_json({'category': 'problem',
- 'definition': {'data': test_def_content}},
+ # create child
+ _ = XModuleDescriptor.load_from_json({'category': 'problem',
+ 'fields': {'data': test_def_content}},
test_course.system, parent_xblock=test_chapter)
# better to pass in persisted parent over the subdag so
# subdag gets the parent pointer (otherwise 2 ops, persist dag, update parent children,
@@ -152,15 +153,24 @@ class TemplateTests(unittest.TestCase):
parent_location=test_course.location, user_id='testbot')
sub = persistent_factories.ItemFactory.create(display_name='subsection 1',
parent_location=chapter.location, user_id='testbot', category='vertical')
- first_problem = persistent_factories.ItemFactory.create(display_name='problem 1',
- parent_location=sub.location, user_id='testbot', category='problem', data="")
+ first_problem = persistent_factories.ItemFactory.create(
+ display_name='problem 1', parent_location=sub.location, user_id='testbot', category='problem',
+ fields={'data':""}
+ )
first_problem.max_attempts = 3
+ first_problem.save() # decache the above into the kvs
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),
- user_id='testbot', category='problem', data="")
+ user_id='testbot', category='problem',
+ fields={'data':""}
+ )
# course root only updated 2x
version_history = modulestore('split').get_block_generations(test_course.location)
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py
index 8a9b35e4f1..73dcabfa69 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py
@@ -11,18 +11,17 @@ from .split_mongo_kvs import SplitMongoKVS, SplitMongoKVSid
log = logging.getLogger(__name__)
-# TODO should this be here or w/ x_module or ???
class CachingDescriptorSystem(MakoDescriptorSystem):
"""
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.
- Computes the metadata inheritance upon creation.
+ Computes the settings (nee 'metadata') inheritance upon creation.
"""
def __init__(self, modulestore, course_entry, module_data, lazy,
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
modules
@@ -50,9 +49,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self.default_class = default_class
# TODO see if self.course_id is needed: is already in course_entry but could be > 1 value
# Compute inheritance
- modulestore.inherit_metadata(course_entry.get('blocks', {}),
- course_entry.get('blocks', {})
- .get(course_entry.get('root')))
+ modulestore.inherit_settings(
+ course_entry.get('blocks', {}),
+ course_entry.get('blocks', {}).get(course_entry.get('root'))
+ )
def _load_item(self, usage_id, course_entry_override=None):
# TODO ensure all callers of system.load_item pass just the id
@@ -73,9 +73,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None):
if course_entry_override is None:
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', {})
- metadata = json_data.get('metadata', {})
block_locator = BlockUsageLocator(
version_guid=course_entry_override['_id'],
@@ -86,9 +85,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
kvs = SplitMongoKVS(
definition,
- json_data.get('children', []),
- metadata,
- json_data.get('_inherited_metadata'),
+ json_data.get('fields', {}),
+ json_data.get('_inherited_settings'),
block_locator,
json_data.get('category'))
model_data = DbModel(kvs, class_, None,
@@ -111,10 +109,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
error_msg=exc_info_to_str(sys.exc_info())
)
- module.edited_by = json_data.get('edited_by')
- module.edited_on = json_data.get('edited_on')
- module.previous_version = json_data.get('previous_version')
- module.update_version = json_data.get('update_version')
+ edit_info = json_data.get('edit_info', {})
+ module.edited_by = edit_info.get('edited_by')
+ module.edited_on = edit_info.get('edited_on')
+ module.previous_version = edit_info.get('previous_version')
+ module.update_version = edit_info.get('update_version')
module.definition_locator = self.modulestore.definition_locator(definition)
# decache any pending field settings
module.save()
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
index 74c7e7241a..a1429d9c90 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
@@ -16,6 +16,9 @@ from .. import ModuleStoreBase
from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader
from .caching_descriptor_system import CachingDescriptorSystem
+from xblock.core import Scope
+from pytz import UTC
+import collections
log = logging.getLogger(__name__)
#==============================================================================
@@ -102,10 +105,12 @@ class SplitMongoModuleStore(ModuleStoreBase):
'''
new_module_data = {}
for usage_id in base_usage_ids:
- new_module_data = self.descendants(system.course_entry['blocks'],
- usage_id,
- depth,
- new_module_data)
+ new_module_data = self.descendants(
+ system.course_entry['blocks'],
+ usage_id,
+ depth,
+ new_module_data
+ )
# remove any which were already in module_data (not sure if there's a better way)
for newkey in new_module_data.iterkeys():
@@ -114,8 +119,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
if lazy:
for block in new_module_data.itervalues():
- block['definition'] = DefinitionLazyLoader(self,
- block['definition'])
+ block['definition'] = DefinitionLazyLoader(self, block['definition'])
else:
# Load all descendants by id
descendent_definitions = self.definitions.find({
@@ -127,7 +131,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
for block in new_module_data.itervalues():
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)
return system.module_data
@@ -317,7 +321,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
definitions.
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
substring matching use {$regex: /acme.*corp/i} type syntax.
@@ -371,7 +375,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
course = self._lookup_course(locator)
items = []
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:
items.append(BlockUsageLocator(url=locator.as_course_locator(), usage_id=parent_id))
return items
@@ -427,11 +431,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
definition = self.definitions.find_one({'_id': definition_locator.definition_id})
if definition is None:
return None
- return {'original_version': definition['original_version'],
- 'previous_version': definition['previous_version'],
- 'edited_by': definition['edited_by'],
- 'edited_on': definition['edited_on']
- }
+ return definition['edit_info']
def get_course_successors(self, course_locator, version_history_depth=1):
'''
@@ -471,29 +471,29 @@ class SplitMongoModuleStore(ModuleStoreBase):
Find the history of this block. Return as a VersionTree of each place the block changed (except
deletion).
- The block's history tracks its explicit changes; so, changes in descendants won't be reflected
- as new iterations.
+ The block's history tracks its explicit changes but not the changes in its children.
+
'''
block_locator = block_locator.version_agnostic()
course_struct = self._lookup_course(block_locator)
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'],
update_version_field: {'$exists': True}})
# find (all) root versions and build map previous: [successors]
possible_roots = []
result = {}
for version in all_versions_with_block:
- if version['_id'] == version['blocks'][usage_id]['update_version']:
- if version['blocks'][usage_id].get('previous_version') is None:
- possible_roots.append(version['blocks'][usage_id]['update_version'])
+ if version['_id'] == version['blocks'][usage_id]['edit_info']['update_version']:
+ if version['blocks'][usage_id]['edit_info'].get('previous_version') is None:
+ possible_roots.append(version['blocks'][usage_id]['edit_info']['update_version'])
else:
- result.setdefault(version['blocks'][usage_id]['previous_version'], set()).add(
- version['blocks'][usage_id]['update_version'])
+ result.setdefault(version['blocks'][usage_id]['edit_info']['previous_version'], set()).add(
+ version['blocks'][usage_id]['edit_info']['update_version'])
# more than one possible_root means usage was added and deleted > 1x.
if len(possible_roots) > 1:
# 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:
possible_roots = [element_to_find]
for possibility in possible_roots:
@@ -513,7 +513,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
Find the version_history_depth next versions of this definition. Return as a VersionTree
'''
# TODO implement
- pass
+ raise NotImplementedError()
def create_definition_from_data(self, new_def_data, category, user_id):
"""
@@ -522,16 +522,21 @@ class SplitMongoModuleStore(ModuleStoreBase):
:param user_id: request.user object
"""
- document = {"category" : category,
- "data": new_def_data,
- "edited_by": user_id,
- "edited_on": datetime.datetime.utcnow(),
- "previous_version": None,
- "original_version": None}
+ new_def_data = self._filter_special_fields(new_def_data)
+ document = {
+ "category" : category,
+ "fields": new_def_data,
+ "edit_info": {
+ "edited_by": user_id,
+ "edited_on": datetime.datetime.now(UTC),
+ "previous_version": None,
+ "original_version": None
+ }
+ }
new_id = self.definitions.insert(document)
definition_locator = DescriptionLocator(new_id)
- document['original_version'] = new_id
- self.definitions.update({'_id': new_id}, {'$set': {"original_version": new_id}})
+ document['edit_info']['original_version'] = new_id
+ self.definitions.update({'_id': new_id}, {'$set': {"edit_info.original_version": new_id}})
return definition_locator
def update_definition_from_data(self, definition_locator, new_def_data, user_id):
@@ -541,16 +546,14 @@ class SplitMongoModuleStore(ModuleStoreBase):
:param user_id: request.user
"""
+ new_def_data = self._filter_special_fields(new_def_data)
def needs_saved():
- if isinstance(new_def_data, dict):
- for key, value in new_def_data.iteritems():
- if key not in old_definition['data'] or value != old_definition['data'][key]:
- return True
- for key, value in old_definition['data'].iteritems():
- if key not in new_def_data:
- return True
- else:
- return new_def_data != old_definition['data']
+ for key, value in new_def_data.iteritems():
+ if key not in old_definition['fields'] or value != old_definition['fields'][key]:
+ return True
+ for key, value in old_definition.get('fields', {}).iteritems():
+ if key not in new_def_data:
+ return True
# 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
@@ -560,10 +563,10 @@ class SplitMongoModuleStore(ModuleStoreBase):
del old_definition['_id']
if needs_saved():
- old_definition['data'] = new_def_data
- old_definition['edited_by'] = user_id
- old_definition['edited_on'] = datetime.datetime.utcnow()
- old_definition['previous_version'] = definition_locator.definition_id
+ old_definition['fields'] = new_def_data
+ old_definition['edit_info']['edited_by'] = user_id
+ old_definition['edit_info']['edited_on'] = datetime.datetime.now(UTC)
+ old_definition['edit_info']['previous_version'] = definition_locator.definition_id
new_id = self.definitions.insert(old_definition)
return DescriptionLocator(new_id), True
else:
@@ -605,11 +608,11 @@ class SplitMongoModuleStore(ModuleStoreBase):
else:
return id_root
- # TODO I would love to write this to take a real descriptor and persist it BUT descriptors, kvs, and dbmodel
- # all assume locators are set and unique! Having this take the model contents piecemeal breaks the separation
- # of model from persistence layer
- def create_item(self, course_or_parent_locator, category, user_id, definition_locator=None, new_def_data=None,
- metadata=None, force=False):
+ # TODO Should I rewrite this to take a new xblock instance rather than to construct it? That is, require the
+ # caller to use XModuleDescriptor.load_from_json thus reducing similar code and making the object creation and
+ # 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, fields=None,
+ force=False):
"""
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.
@@ -624,9 +627,10 @@ class SplitMongoModuleStore(ModuleStoreBase):
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.
- If new_def_data is None, then definition_locator must have a value meaning that this block points
- to the existing definition. If new_def_data is not None and definition_location is not None, then
- new_def_data is assumed to be a new payload for definition_location.
+ If fields does not contain any Scope.content, then definition_locator must have a value meaning that this
+ block points
+ 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
to the definition which may be new or a new version of an existing or an existing.
@@ -645,6 +649,8 @@ class SplitMongoModuleStore(ModuleStoreBase):
index_entry = self._get_index_if_valid(course_or_parent_locator, force)
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
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)
@@ -655,23 +661,27 @@ class SplitMongoModuleStore(ModuleStoreBase):
new_structure = self._version_structure(structure, user_id)
# generate an id
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:
parent = new_structure['blocks'][course_or_parent_locator.usage_id]
- parent['children'].append(new_usage_id)
- parent['edited_on'] = datetime.datetime.utcnow()
- parent['edited_by'] = user_id
- parent['previous_version'] = parent['update_version']
- update_version_keys.append('blocks.{}.update_version'.format(course_or_parent_locator.usage_id))
+ parent['fields'].setdefault('children', []).append(new_usage_id)
+ parent['edit_info']['edited_on'] = datetime.datetime.now(UTC)
+ parent['edit_info']['edited_by'] = user_id
+ parent['edit_info']['previous_version'] = parent['edit_info']['update_version']
+ 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] = {
- "children": [],
"category": category,
"definition": definition_locator.definition_id,
- "metadata": metadata if metadata else {},
- 'edited_on': datetime.datetime.utcnow(),
- 'edited_by': user_id,
- 'previous_version': None
+ "fields": block_fields,
+ 'edit_info': {
+ 'edited_on': datetime.datetime.now(UTC),
+ 'edited_by': user_id,
+ 'previous_version': None
}
+ }
new_id = self.structures.insert(new_structure)
update_version_payload = {key: new_id for key in update_version_keys}
self.structures.update({'_id': new_id},
@@ -689,8 +699,9 @@ class SplitMongoModuleStore(ModuleStoreBase):
usage_id=new_usage_id,
version_guid=new_id))
- def create_course(self, org, prettyid, user_id, id_root=None, metadata=None, course_data=None,
- master_version='draft', versions_dict=None, root_category='course'):
+ def create_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
the course root of the resulting entry (the location has the course id)
@@ -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,
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
- metadata and a starting version are provided, it will generate a successor version to the given version,
- and update the metadata with any provided values (via update not setting).
+ fields: if scope.settings fields provided, will set the fields of the root course object in the
+ new course. If both
+ 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
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
- course). If not provided and no draft is given, it will be empty and get the field defaults (hopefully) when
+ a version_dict, it will reuse the same definition as that version's course
+ (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.
- 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.
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
- 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.
"""
- if metadata is None:
- metadata = {}
+ partitioned_fields = self._partition_fields_by_scope('course', fields)
+ 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
# 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
- if course_data is None:
- course_data = {}
definition_entry = {
'category': root_category,
- 'data': course_data,
- 'edited_by': user_id,
- 'edited_on': datetime.datetime.utcnow(),
- 'previous_version': None,
+ 'fields': definition_fields,
+ 'edit_info': {
+ 'edited_by': user_id,
+ 'edited_on': datetime.datetime.now(UTC),
+ 'previous_version': None,
}
+ }
definition_id = self.definitions.insert(definition_entry)
- definition_entry['original_version'] = definition_id
- self.definitions.update({'_id': definition_id}, {'$set': {"original_version": definition_id}})
+ definition_entry['edit_info']['original_version'] = definition_id
+ self.definitions.update({'_id': definition_id}, {'$set': {"edit_info.original_version": definition_id}})
draft_structure = {
'root': 'course',
'previous_version': None,
'edited_by': user_id,
- 'edited_on': datetime.datetime.utcnow(),
+ 'edited_on': datetime.datetime.now(UTC),
'blocks': {
'course': {
- 'children':[],
'category': 'course',
'definition': definition_id,
- 'metadata': metadata,
- 'edited_on': datetime.datetime.utcnow(),
- 'edited_by': user_id,
- 'previous_version': None}}}
+ 'fields': block_fields,
+ 'edit_info': {
+ 'edited_on': datetime.datetime.now(UTC),
+ 'edited_by': user_id,
+ 'previous_version': None
+ }
+ }
+ }
+ }
new_id = self.structures.insert(draft_structure)
draft_structure['original_version'] = new_id
self.structures.update({'_id': 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:
- versions_dict = {master_version: new_id}
+ versions_dict = {master_branch: new_id}
else:
- versions_dict[master_version] = new_id
+ versions_dict[master_branch] = new_id
else:
# 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)
- if course_data is not None or metadata:
+ if definition_fields or block_fields:
draft_structure = self._version_structure(draft_structure, user_id)
root_block = draft_structure['blocks'][draft_structure['root']]
- if metadata is not None:
- root_block['metadata'].update(metadata)
- if course_data is not None:
+ if block_fields is not None:
+ root_block['fields'].update(block_fields)
+ if definition_fields is not None:
definition = self.definitions.find_one({'_id': root_block['definition']})
- definition['data'].update(course_data)
- definition['previous_version'] = definition['_id']
- definition['edited_by'] = user_id
- definition['edited_on'] = datetime.datetime.utcnow()
+ definition['fields'].update(definition_fields)
+ definition['edit_info']['previous_version'] = definition['_id']
+ definition['edit_info']['edited_by'] = user_id
+ definition['edit_info']['edited_on'] = datetime.datetime.now(UTC)
del definition['_id']
root_block['definition'] = self.definitions.insert(definition)
- root_block['edited_on'] = datetime.datetime.utcnow()
- root_block['edited_by'] = user_id
- root_block['previous_version'] = root_block.get('update_version')
+ root_block['edit_info']['edited_on'] = datetime.datetime.now(UTC)
+ root_block['edit_info']['edited_by'] = user_id
+ root_block['edit_info']['previous_version'] = root_block['edit_info'].get('update_version')
# insert updates the '_id' in 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},
- {'$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
if id_root is None:
id_root = org
@@ -795,14 +819,14 @@ class SplitMongoModuleStore(ModuleStoreBase):
'org': org,
'prettyid': prettyid,
'edited_by': user_id,
- 'edited_on': datetime.datetime.utcnow(),
+ 'edited_on': datetime.datetime.now(UTC),
'versions': versions_dict}
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):
"""
- 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).
raises ItemNotFoundError if the location does not exist.
@@ -819,31 +843,38 @@ class SplitMongoModuleStore(ModuleStoreBase):
index_entry = self._get_index_if_valid(descriptor.location, force)
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
original_entry = original_structure['blocks'][descriptor.location.usage_id]
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
# check metadata
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 is_updated:
new_structure = self._version_structure(original_structure, user_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["metadata"] = descriptor.xblock_kvs.get_own_metadata()
- block_data['edited_on'] = datetime.datetime.utcnow()
- block_data['edited_by'] = user_id
- block_data['previous_version'] = block_data['update_version']
+ block_data["fields"] = descriptor.get_explicitly_set_fields_by_scope(Scope.settings)
+ if descriptor.has_children:
+ block_data['fields']["children"] = [self._usage_id(child) for child in descriptor.children]
+
+ 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)
- self.structures.update({'_id': new_id},
- {'$set': {'blocks.{}.update_version'.format(descriptor.location.usage_id): new_id}})
+ self.structures.update(
+ {'_id': new_id},
+ {'$set': {'blocks.{}.edit_info.update_version'.format(descriptor.location.usage_id): new_id}})
# update the index entry if appropriate
if index_entry is not None:
@@ -869,8 +900,8 @@ class SplitMongoModuleStore(ModuleStoreBase):
returns the post-persisted version of the incoming xblock. Note that its children will be ids not
objects.
- :param xblock:
- :param user_id:
+ :param xblock: the head of the dag
+ :param user_id: who's doing the change
"""
# find course_index entry if applicable and structures entry
index_entry = self._get_index_if_valid(xblock.location, force)
@@ -883,7 +914,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
new_id = self.structures.insert(new_structure)
update_command = {}
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})
# update the index entry if appropriate
@@ -897,14 +928,14 @@ class SplitMongoModuleStore(ModuleStoreBase):
def _persist_subdag(self, xblock, user_id, structure_blocks):
# 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):
- xblock.definition_locator = self.create_definition_from_data(new_def_data,
- xblock.category, user_id)
+ xblock.definition_locator = self.create_definition_from_data(
+ new_def_data, xblock.category, user_id)
is_updated = True
- elif new_def_data is not None:
- xblock.definition_locator, is_updated = self.update_definition_from_data(xblock.definition_locator,
- new_def_data, user_id)
+ elif new_def_data:
+ xblock.definition_locator, is_updated = self.update_definition_from_data(
+ xblock.definition_locator, new_def_data, user_id)
if xblock.location.usage_id is None:
# generate an id
@@ -916,7 +947,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
is_new = False
usage_id = xblock.location.usage_id
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
children = []
@@ -930,41 +961,52 @@ class SplitMongoModuleStore(ModuleStoreBase):
children.append(child)
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:
- 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:
+ previous_version = None if is_new else structure_blocks[usage_id]['edit_info'].get('update_version')
structure_blocks[usage_id] = {
- "children": children,
"category": xblock.category,
"definition": xblock.definition_locator.definition_id,
- "metadata": metadata if metadata else {},
- 'previous_version': structure_blocks.get(usage_id, {}).get('update_version'),
- 'edited_by': user_id,
- 'edited_on': datetime.datetime.utcnow()
+ "fields": block_fields,
+ 'edit_info': {
+ 'previous_version': previous_version,
+ 'edited_by': user_id,
+ 'edited_on': datetime.datetime.now(UTC)
+ }
}
updated_blocks.append(usage_id)
return updated_blocks
- def _compare_metadata(self, metadata, original_metadata):
- original_keys = original_metadata.keys()
- if len(metadata) != len(original_keys):
+ def _compare_settings(self, settings, original_fields):
+ """
+ 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
else:
- new_keys = metadata.keys()
+ new_keys = settings.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
- # TODO change all callers to update_item
- def update_children(self, course_id, location, children):
- raise NotImplementedError()
+ def update_children(self, location, children):
+ '''Deprecated, use update_item.'''
+ raise NotImplementedError('use update_item')
- # TODO change all callers to update_item
- def update_metadata(self, course_id, location, metadata):
- raise NotImplementedError()
+ def update_metadata(self, location, metadata):
+ '''Deprecated, use update_item.'''
+ raise NotImplementedError('use update_item')
def update_course_index(self, course_locator, new_values_dict, update_versions=False):
"""
@@ -992,9 +1034,9 @@ class SplitMongoModuleStore(ModuleStoreBase):
self.course_index.update({'_id': course_locator.course_id},
{'$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.
returns CourseLocator for new version
@@ -1018,17 +1060,18 @@ class SplitMongoModuleStore(ModuleStoreBase):
update_version_keys = []
for parent in parents:
parent_block = new_blocks[parent.usage_id]
- parent_block['children'].remove(usage_locator.usage_id)
- parent_block['edited_on'] = datetime.datetime.utcnow()
- parent_block['edited_by'] = user_id
- parent_block['previous_version'] = parent_block['update_version']
- update_version_keys.append('blocks.{}.update_version'.format(parent.usage_id))
+ parent_block['fields']['children'].remove(usage_locator.usage_id)
+ parent_block['edit_info']['edited_on'] = datetime.datetime.now(UTC)
+ parent_block['edit_info']['edited_by'] = user_id
+ parent_block['edit_info']['previous_version'] = parent_block['edit_info']['update_version']
+ update_version_keys.append('blocks.{}.edit_info.update_version'.format(parent.usage_id))
# remove subtree
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)
del new_blocks[usage_id]
- remove_subtree(usage_locator.usage_id)
+ if delete_children:
+ remove_subtree(usage_locator.usage_id)
# update index if appropriate and structures
new_id = self.structures.insert(new_structure)
@@ -1062,32 +1105,38 @@ class SplitMongoModuleStore(ModuleStoreBase):
# this is the only real delete in the system. should it do something else?
self.course_index.remove(index['_id'])
- def inherit_metadata(self, block_map, block, inheriting_metadata=None):
+ def get_errored_courses(self):
"""
- Updates block with any value
- 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
+ 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 inheritable setting set by an ancestor and recurses to children.
"""
if block is None:
return
- if inheriting_metadata is None:
- inheriting_metadata = {}
+ if inheriting_settings is None:
+ inheriting_settings = {}
# 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.,
# 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
- inheriting_metadata = block['_inherited_metadata'].copy()
+ inheriting_settings = block['_inherited_settings'].copy()
+ block_fields = block['fields']
for field in inheritance.INHERITABLE_METADATA:
- if field in block['metadata']:
- inheriting_metadata[field] = block['metadata'][field]
+ if field in block_fields:
+ inheriting_settings[field] = block_fields[field]
- for child in block.get('children', []):
- self.inherit_metadata(block_map, block_map[child], inheriting_metadata)
+ for child in block_fields.get('children', []):
+ self.inherit_settings(block_map, block_map[child], inheriting_settings)
def descendants(self, block_map, usage_id, depth, descendent_map):
"""
@@ -1104,7 +1153,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
if depth is None or depth > 0:
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)
@@ -1217,7 +1266,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
del new_structure['_id']
new_structure['previous_version'] = structure['_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
def _find_local_root(self, element_to_find, possibility, tree):
@@ -1242,3 +1291,31 @@ class SplitMongoModuleStore(ModuleStoreBase):
self.course_index.update(
{"_id": index_entry["_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
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_mongo_kvs.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_mongo_kvs.py
index 843c1ce364..0ffc111586 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_mongo_kvs.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_mongo_kvs.py
@@ -8,45 +8,49 @@ from .definition_lazy_loader import DefinitionLazyLoader
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):
"""
A KeyValueStore that maps keyed data access to one of the 3 data areas
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 children:
- :param metadata: the locally defined value for each metadata field
- :param _inherited_metadata: the value of each inheritable field from above this.
- Note, metadata may override and disagree w/ this b/c this says what the value
- should be if metadata is undefined for this field.
+ :param definition: either a lazyloader or definition id for the definition
+ :param fields: a dictionary of the locally set fields
+ :param _inherited_settings: 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
+ should be if the field is undefined.
"""
# 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
# that kvs's should be independent thus responsible for the isolation.
- if isinstance(definition, DefinitionLazyLoader):
- self._definition = definition
- else:
- self._definition = copy.copy(definition)
- self._children = copy.copy(children)
- self._metadata = copy.copy(metadata)
- self._inherited_metadata = _inherited_metadata
+ self._definition = definition # either a DefinitionLazyLoader or the db id of the definition.
+ # if the db id, then the definition is presumed to be loaded into _fields
+ self._fields = copy.copy(fields)
+ self._inherited_settings = _inherited_settings
self._location = location
self._category = category
def get(self, key):
- if key.scope == Scope.children:
- return self._children
- elif key.scope == Scope.parent:
+ # simplest case, field is directly set
+ if key.field_name in self._fields:
+ return self._fields[key.field_name]
+
+ # parent undefined in editing runtime (I think)
+ if key.scope == Scope.parent:
return None
+ if key.scope == Scope.children:
+ raise KeyError()
elif key.scope == Scope.settings:
- if key.field_name in self._metadata:
- return self._metadata[key.field_name]
- elif key.field_name in self._inherited_metadata:
- return self._inherited_metadata[key.field_name]
+ # get from inheritance since not locally set
+ if key.field_name in self._inherited_settings:
+ return self._inherited_settings[key.field_name]
else:
raise KeyError()
elif key.scope == Scope.content:
@@ -54,110 +58,118 @@ class SplitMongoKVS(KeyValueStore):
return self._location
elif key.field_name == 'category':
return self._category
- else:
- if isinstance(self._definition, DefinitionLazyLoader):
- self._definition = self._definition.fetch()
- if (key.field_name == 'data' and
- 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()
- else:
- return self._definition['data'][key.field_name]
+ elif isinstance(self._definition, DefinitionLazyLoader):
+ self._load_definition()
+ if key.field_name in self._fields:
+ return self._fields[key.field_name]
+
+ raise KeyError()
else:
raise InvalidScopeError(key.scope)
def set(self, key, value):
- # TODO cache db update implications & add method to invoke
- if key.scope == Scope.children:
- self._children = value
- # TODO remove inheritance from any orphaned exchildren
- # 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':
- self._location = value
- elif key.field_name == 'category':
- self._category = value
- else:
- 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:
- self._definition.setdefault('data', {})[key.field_name] = value
- else:
+ # handle any special cases
+ if key.scope not in [Scope.children, Scope.settings, Scope.content]:
raise InvalidScopeError(key.scope)
+ if key.scope == Scope.content:
+ if key.field_name == 'location':
+ self._location = value # is changing this legal?
+ return
+ elif key.field_name == 'category':
+ # TODO should this raise an exception? that is, should xblock types be mungable?
+ return
+ else:
+ self._load_definition()
+
+ # 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):
- # TODO cache db update implications & add method to invoke
- if key.scope == Scope.children:
- self._children = []
- elif key.scope == Scope.settings:
- # 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':
- pass
- elif key.field_name == 'category':
- pass
- else:
- 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.setdefault('data', None)
- else:
- try:
- del self._definition['data'][key.field_name]
- except KeyError:
- pass
- else:
+ # handle any special cases
+ if key.scope not in [Scope.children, Scope.settings, Scope.content]:
raise InvalidScopeError(key.scope)
+ if key.scope == Scope.content:
+ if key.field_name == 'location':
+ return # noop
+ elif key.field_name == 'category':
+ # TODO should this raise an exception? that is, should xblock types be mungable?
+ return # noop
+ else:
+ self._load_definition()
+
+ # delete the field value
+ if key.field_name in self._fields:
+ del self._fields[key.field_name]
+
+ # handle any side effects
+ # if key.scope == Scope.children:
+ # TODO remove inheritance from any exchildren
+ # if key.scope == Scope.settings:
+ # TODO if inheritable, push down _inherited_settings value to children
def has(self, key):
- if key.scope in (Scope.children, Scope.parent):
- return True
- 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:
+ # handle any special cases
+ if key.scope == Scope.content:
if key.field_name == 'location':
return True
elif key.field_name == 'category':
return self._category is not None
else:
- if isinstance(self._definition, DefinitionLazyLoader):
- self._definition = self._definition.fetch()
- if (key.field_name == 'data' and
- not isinstance(self._definition.get('data'), dict)):
- return self._definition.get('data') is not None
+ self._load_definition()
+ elif key.scope == Scope.parent:
+ return True
+
+ # it's not clear whether inherited values should return True. Right now they don't
+ # 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._fields
+
+ # 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):
+ """
+ Where the field value comes from: one of [PROVENANCE_LOCAL, PROVENANCE_DEFAULT, PROVENANCE_INHERITED].
+ """
+ # handle any special cases
+ if key_scope == Scope.content:
+ 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 key.field_name in self._definition.get('data', {})
+ 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 False
+ return PROVENANCE_DEFAULT
- def get_data(self):
- """
- Intended only for use by persistence layer to get the native definition['data'] rep
- """
- if isinstance(self._definition, DefinitionLazyLoader):
- self._definition = self._definition.fetch()
- return self._definition.get('data')
-
- def get_own_metadata(self):
- """
- Get the metadata explicitly set on this element.
- """
- return self._metadata
-
- def get_inherited_metadata(self):
+ def get_inherited_settings(self):
"""
Get the metadata set by the ancestors (which own metadata may override or not)
"""
- return self._inherited_metadata
+ return self._inherited_settings
+
+ def _load_definition(self):
+ """
+ Update fields w/ the lazily loaded definitions
+ """
+ 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
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/persistent_factories.py b/common/lib/xmodule/xmodule/modulestore/tests/persistent_factories.py
index 5e46f5a318..4675c6a50c 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/persistent_factories.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/persistent_factories.py
@@ -16,8 +16,7 @@ class PersistentCourseFactory(factory.Factory):
* prettyid: defaults to 999
* display_name
* user_id
- * data (optional) the data payload to save in the course item
- * metadata (optional) the metadata payload. If display_name is in the metadata, that takes
+ * fields (optional) the settings and content payloads. If display_name is in the metadata, that takes
precedence over any display_name provided directly.
"""
FACTORY_FOR = CourseDescriptor
@@ -28,7 +27,7 @@ class PersistentCourseFactory(factory.Factory):
user_id = "test_user"
data = None
metadata = None
- master_version = 'draft'
+ master_branch = 'draft'
# pylint: disable=W0613
@classmethod
@@ -38,17 +37,14 @@ class PersistentCourseFactory(factory.Factory):
prettyid = kwargs.get('prettyid')
display_name = kwargs.get('display_name')
user_id = kwargs.get('user_id')
- data = kwargs.get('data')
- metadata = kwargs.get('metadata', {})
- if metadata is None:
- metadata = {}
- if 'display_name' not in metadata:
- metadata['display_name'] = display_name
+ fields = kwargs.get('fields', {})
+ if display_name and 'display_name' not in fields:
+ fields['display_name'] = display_name
# Write the data to the mongo datastore
new_course = modulestore('split').create_course(
- org, prettyid, user_id, metadata=metadata, course_data=data, id_root=prettyid,
- master_version=kwargs.get('master_version'))
+ org, prettyid, user_id, fields=fields, id_root=prettyid,
+ master_branch=kwargs.get('master_branch'))
return new_course
@@ -70,26 +66,23 @@ class ItemFactory(factory.Factory):
"""
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
-
- *metadata* (optional): dictionary of metadata attributes (display_name here takes
- precedence over the above attr)
+ :param display_name (optional): the display name of the item
"""
- metadata = kwargs.get('metadata', {})
- if 'display_name' not in metadata and 'display_name' in kwargs:
- metadata['display_name'] = kwargs['display_name']
+ fields = kwargs.get('fields', {})
+ if 'display_name' not in fields and 'display_name' in kwargs:
+ fields['display_name'] = kwargs['display_name']
return modulestore('split').create_item(kwargs['parent_location'], kwargs['category'],
kwargs['user_id'], definition_locator=kwargs.get('definition_locator'),
- new_def_data=kwargs.get('data'), metadata=metadata)
+ fields=fields)
@classmethod
def _build(cls, target_class, *args, **kwargs):
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
index 9976a33a00..36130be8d4 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
@@ -187,6 +187,7 @@ class SplitModuleCourseTests(SplitModuleTest):
self.assertEqual(course.category, 'course')
self.assertEqual(len(course.tabs), 6)
self.assertEqual(course.display_name, "The Ancient Greek Hero")
+ self.assertEqual(course.lms.graceperiod, datetime.timedelta(hours=2))
self.assertIsNone(course.advertised_start)
self.assertEqual(len(course.children), 0)
self.assertEqual(course.definition_locator.definition_id, "head12345_11")
@@ -438,12 +439,12 @@ class SplitModuleItemTests(SplitModuleTest):
qualifiers=
{
'category': 'chapter',
- 'metadata': {'display_name': {'$regex': 'Hera'}}
+ 'fields': {'display_name': {'$regex': 'Hera'}}
}
)
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(matches[0].location.usage_id, 'head12345')
@@ -507,8 +508,7 @@ class TestItemCrud(SplitModuleTest):
def test_create_minimal_item(self):
"""
- create_item(course_or_parent_locator, category, user, definition_locator=None, new_def_data=None,
- metadata=None): new_desciptor
+ create_item(course_or_parent_locator, category, user, definition_locator=None, fields): new_desciptor
"""
# grab link to course to ensure new versioning works
locator = CourseLocator(course_id="GreekHero", branch='draft')
@@ -518,7 +518,7 @@ class TestItemCrud(SplitModuleTest):
category = 'sequential'
new_module = modulestore().create_item(
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
self.assertEqual(new_module.location.course_id, "GreekHero")
@@ -553,7 +553,7 @@ class TestItemCrud(SplitModuleTest):
category = 'chapter'
new_module = modulestore().create_item(
locator, category, 'user123',
- metadata={'display_name': 'new chapter'},
+ fields={'display_name': 'new chapter'},
definition_locator=DescriptionLocator("chapter12345_2")
)
# check that course version changed and course's previous is the other one
@@ -574,15 +574,13 @@ class TestItemCrud(SplitModuleTest):
new_payload = "empty"
new_module = modulestore().create_item(
locator, category, 'anotheruser',
- metadata={'display_name': 'problem 1'},
- new_def_data=new_payload
+ fields={'display_name': 'problem 1', 'data': new_payload},
)
another_payload = "not empty"
another_module = modulestore().create_item(
locator, category, 'anotheruser',
- metadata={'display_name': 'problem 2'},
+ fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=DescriptionLocator("problem12345_3_1"),
- new_def_data=another_payload
)
# check that course version changed and course's previous is the other one
parent = modulestore().get_item(locator)
@@ -616,6 +614,7 @@ class TestItemCrud(SplitModuleTest):
self.assertNotEqual(problem.max_attempts, 4, "Invalidates rest of test")
problem.max_attempts = 4
+ problem.save() # decache above setting into the kvs
updated_problem = modulestore().update_item(problem, 'changeMaven')
# check that course version changed and course's previous is the other one
self.assertEqual(updated_problem.definition_locator.definition_id, pre_def_id)
@@ -651,6 +650,7 @@ class TestItemCrud(SplitModuleTest):
# reorder children
self.assertGreater(len(block.children), 0, "meaningless test")
moved_child = block.children.pop()
+ block.save() # decache model changes
updated_problem = modulestore().update_item(block, 'childchanger')
# check that course version changed and course's previous is the other one
self.assertEqual(updated_problem.definition_locator.definition_id, pre_def_id)
@@ -660,6 +660,7 @@ class TestItemCrud(SplitModuleTest):
locator.usage_id = "chapter1"
other_block = modulestore().get_item(locator)
other_block.children.append(moved_child)
+ other_block.save() # decache model changes
other_updated = modulestore().update_item(other_block, 'childchanger')
self.assertIn(moved_child, other_updated.children)
@@ -673,6 +674,7 @@ class TestItemCrud(SplitModuleTest):
pre_version_guid = block.location.version_guid
block.grading_policy['GRADER'][0]['min_count'] = 13
+ block.save() # decache model changes
updated_block = modulestore().update_item(block, 'definition_changer')
self.assertNotEqual(updated_block.definition_locator.definition_id, pre_def_id)
@@ -689,15 +691,13 @@ class TestItemCrud(SplitModuleTest):
new_payload = "empty"
modulestore().create_item(
locator, category, 'test_update_manifold',
- metadata={'display_name': 'problem 1'},
- new_def_data=new_payload
+ fields={'display_name': 'problem 1', 'data': new_payload},
)
another_payload = "not empty"
modulestore().create_item(
locator, category, 'test_update_manifold',
- metadata={'display_name': 'problem 2'},
+ fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=DescriptionLocator("problem12345_3_1"),
- new_def_data=another_payload
)
# pylint: disable=W0212
modulestore()._clear_cache()
@@ -712,6 +712,7 @@ class TestItemCrud(SplitModuleTest):
block.children = block.children[1:] + [block.children[0]]
block.advertised_start = "Soon"
+ block.save() # decache model changes
updated_block = modulestore().update_item(block, "test_update_manifold")
self.assertNotEqual(updated_block.definition_locator.definition_id, pre_def_id)
self.assertNotEqual(updated_block.location.version_guid, pre_version_guid)
@@ -733,7 +734,7 @@ class TestItemCrud(SplitModuleTest):
# delete a leaf
problems = modulestore().get_items(reusable_location, {'category': 'problem'})
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,
branch=reusable_location.branch,
usage_id=locn_to_del.usage_id)
@@ -748,7 +749,7 @@ class TestItemCrud(SplitModuleTest):
# delete a subtree
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
def check_subtree(node):
@@ -855,7 +856,7 @@ class TestCourseCreation(SplitModuleTest):
# using new_draft.location will insert the chapter under the course root
new_item = modulestore().create_item(
new_draft.location, 'chapter', 'leech_master',
- metadata={'display_name': 'new chapter'}
+ fields={'display_name': 'new chapter'}
)
new_draft_locator.version_guid = None
new_index = modulestore().get_course_index_info(new_draft_locator)
@@ -887,20 +888,18 @@ class TestCourseCreation(SplitModuleTest):
original_locator = CourseLocator(course_id="contender", branch='draft')
original = modulestore().get_course(original_locator)
original_index = modulestore().get_course_index_info(original_locator)
- data_payload = {}
- metadata_payload = {}
+ fields = {}
for field in original.fields:
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:
- metadata_payload[field.name] = getattr(original, field.name)
- data_payload['grading_policy']['GRADE_CUTOFFS'] = {'A': .9, 'B': .8, 'C': .65}
- metadata_payload['display_name'] = 'Derivative'
+ fields[field.name] = getattr(original, field.name)
+ fields['grading_policy']['GRADE_CUTOFFS'] = {'A': .9, 'B': .8, 'C': .65}
+ fields['display_name'] = 'Derivative'
new_draft = modulestore().create_course(
'leech', 'derivative', 'leech_master', id_root='counter',
versions_dict={'draft': original_index['versions']['draft']},
- course_data=data_payload,
- metadata=metadata_payload
+ fields=fields
)
new_draft_locator = new_draft.location
self.assertRegexpMatches(new_draft_locator.course_id, r'counter.*')
@@ -913,10 +912,10 @@ class TestCourseCreation(SplitModuleTest):
self.assertGreaterEqual(new_index["edited_on"], pre_time)
self.assertLessEqual(new_index["edited_on"], datetime.datetime.now(UTC))
self.assertEqual(new_index['edited_by'], 'leech_master')
- self.assertEqual(new_draft.display_name, metadata_payload['display_name'])
+ self.assertEqual(new_draft.display_name, fields['display_name'])
self.assertDictEqual(
new_draft.grading_policy['GRADE_CUTOFFS'],
- data_payload['grading_policy']['GRADE_CUTOFFS']
+ fields['grading_policy']['GRADE_CUTOFFS']
)
def test_update_course_index(self):
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 637eb9f17c..c4deaa8925 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -587,33 +587,20 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
Creates an instance of this descriptor from the supplied json_data.
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:
- 'category': the xmodule category (required)
- - 'metadata': a dict of locally set metadata (not inherited)
- - 'children': a list of children's usage_ids w/in this course
- - 'definition':
+ - 'fields': a dict of locally set fields (not inherited)
+ - 'definition': (optional) the db id for the definition record (not the definition content) or a
+ definitionLazyLoader
- '_id' (optional): the usage_id of this. Will generate one if not given one.
"""
usage_id = json_data.get('_id', None)
- if not '_inherited_metadata' in json_data and parent_xblock is not None:
- json_data['_inherited_metadata'] = parent_xblock.xblock_kvs.get_inherited_metadata().copy()
- json_metadata = json_data.get('metadata', {})
+ if not '_inherited_settings' in json_data and parent_xblock is not None:
+ json_data['_inherited_settings'] = parent_xblock.xblock_kvs.get_inherited_settings().copy()
+ json_fields = json_data.get('fields', {})
for field in inheritance.INHERITABLE_METADATA:
- if field in json_metadata:
- json_data['_inherited_metadata'][field] = json_metadata[field]
+ if field in json_fields:
+ json_data['_inherited_settings'][field] = json_fields[field]
new_block = system.xblock_from_json(cls, usage_id, json_data)
if parent_xblock is not None:
@@ -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).
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
def editable_metadata_fields(self):
"""
diff --git a/common/test/data/splitmongo_json/definitions.json b/common/test/data/splitmongo_json/definitions.json
index 0ed42112aa..433afbe8ac 100644
--- a/common/test/data/splitmongo_json/definitions.json
+++ b/common/test/data/splitmongo_json/definitions.json
@@ -2,7 +2,7 @@
{
"_id":"head12345_12",
"category":"course",
- "data":{
+ "fields":{
"textbooks":[
],
@@ -43,15 +43,17 @@
},
"wiki_slug":null
},
- "edited_by":"testassist@edx.org",
- "edited_on":{"$date" : 1364481713238},
- "previous_version":"head12345_11",
- "original_version":"head12345_10"
+ "edit_info": {
+ "edited_by":"testassist@edx.org",
+ "edited_on":{"$date" : 1364481713238},
+ "previous_version":"head12345_11",
+ "original_version":"head12345_10"
+ }
},
{
"_id":"head12345_11",
"category":"course",
- "data":{
+ "fields":{
"textbooks":[
],
@@ -92,15 +94,17 @@
},
"wiki_slug":null
},
- "edited_by":"testassist@edx.org",
- "edited_on":{"$date" : 1364481713238},
- "previous_version":"head12345_10",
- "original_version":"head12345_10"
+ "edit_info": {
+ "edited_by":"testassist@edx.org",
+ "edited_on":{"$date" : 1364481713238},
+ "previous_version":"head12345_10",
+ "original_version":"head12345_10"
+ }
},
{
"_id":"head12345_10",
"category":"course",
- "data":{
+ "fields":{
"textbooks":[
],
@@ -141,15 +145,17 @@
},
"wiki_slug":null
},
- "edited_by":"test@edx.org",
- "edited_on":{"$date": 1364473713238},
- "previous_version":null,
- "original_version":"head12345_10"
+ "edit_info": {
+ "edited_by":"test@edx.org",
+ "edited_on":{"$date": 1364473713238},
+ "previous_version":null,
+ "original_version":"head12345_10"
+ }
},
{
"_id":"head23456_1",
"category":"course",
- "data":{
+ "fields":{
"textbooks":[
],
@@ -190,15 +196,17 @@
},
"wiki_slug":null
},
- "edited_by":"test@edx.org",
- "edited_on":{"$date": 1364481313238},
- "previous_version":"head23456_0",
- "original_version":"head23456_0"
+ "edit_info": {
+ "edited_by":"test@edx.org",
+ "edited_on":{"$date": 1364481313238},
+ "previous_version":"head23456_0",
+ "original_version":"head23456_0"
+ }
},
{
"_id":"head23456_0",
"category":"course",
- "data":{
+ "fields":{
"textbooks":[
],
@@ -239,15 +247,17 @@
},
"wiki_slug":null
},
- "edited_by":"test@edx.org",
- "edited_on":{"$date" : 1364481313238},
- "previous_version":null,
- "original_version":"head23456_0"
+ "edit_info": {
+ "edited_by":"test@edx.org",
+ "edited_on":{"$date" : 1364481313238},
+ "previous_version":null,
+ "original_version":"head23456_0"
+ }
},
{
"_id":"head345679_1",
"category":"course",
- "data":{
+ "fields":{
"textbooks":[
],
@@ -281,54 +291,66 @@
},
"wiki_slug":null
},
- "edited_by":"test@edx.org",
- "edited_on":{"$date" : 1364481313238},
- "previous_version":null,
- "original_version":"head23456_0"
+ "edit_info": {
+ "edited_by":"test@edx.org",
+ "edited_on":{"$date" : 1364481313238},
+ "previous_version":null,
+ "original_version":"head23456_0"
+ }
},
{
"_id":"chapter12345_1",
"category":"chapter",
- "data":null,
- "edited_by":"testassist@edx.org",
- "edited_on":{"$date" : 1364483713238},
- "previous_version":null,
- "original_version":"chapter12345_1"
+ "fields":{},
+ "edit_info": {
+ "edited_by":"testassist@edx.org",
+ "edited_on":{"$date" : 1364483713238},
+ "previous_version":null,
+ "original_version":"chapter12345_1"
+ }
},
{
"_id":"chapter12345_2",
"category":"chapter",
- "data":null,
- "edited_by":"testassist@edx.org",
- "edited_on":{"$date" : 1364483713238},
- "previous_version":null,
- "original_version":"chapter12345_2"
+ "fields":{},
+ "edit_info": {
+ "edited_by":"testassist@edx.org",
+ "edited_on":{"$date" : 1364483713238},
+ "previous_version":null,
+ "original_version":"chapter12345_2"
+ }
},
{
"_id":"chapter12345_3",
"category":"chapter",
- "data":null,
- "edited_by":"testassist@edx.org",
- "edited_on":{"$date" : 1364483713238},
- "previous_version":null,
- "original_version":"chapter12345_3"
+ "fields":{},
+ "edit_info": {
+ "edited_by":"testassist@edx.org",
+ "edited_on":{"$date" : 1364483713238},
+ "previous_version":null,
+ "original_version":"chapter12345_3"
+ }
},
{
"_id":"problem12345_3_1",
"category":"problem",
- "data":"",
- "edited_by":"testassist@edx.org",
- "edited_on":{"$date" : 1364483713238},
- "previous_version":null,
- "original_version":"problem12345_3_1"
+ "fields": {"data": ""},
+ "edit_info": {
+ "edited_by":"testassist@edx.org",
+ "edited_on":{"$date" : 1364483713238},
+ "previous_version":null,
+ "original_version":"problem12345_3_1"
+ }
},
{
"_id":"problem12345_3_2",
"category":"problem",
- "data":"",
- "edited_by":"testassist@edx.org",
- "edited_on":{"$date" : 1364483713238},
- "previous_version":null,
- "original_version":"problem12345_3_2"
+ "fields": {"data": ""},
+ "edit_info": {
+ "edited_by":"testassist@edx.org",
+ "edited_on":{"$date" : 1364483713238},
+ "previous_version":null,
+ "original_version":"problem12345_3_2"
+ }
}
]
\ No newline at end of file
diff --git a/common/test/data/splitmongo_json/structures.json b/common/test/data/splitmongo_json/structures.json
index 0021225213..b72c0fd7a5 100644
--- a/common/test/data/splitmongo_json/structures.json
+++ b/common/test/data/splitmongo_json/structures.json
@@ -10,14 +10,14 @@
},
"blocks":{
"head12345":{
- "children":[
- "chapter1",
- "chapter2",
- "chapter3"
- ],
"category":"course",
"definition":"head12345_12",
- "metadata":{
+ "fields":{
+ "children":[
+ "chapter1",
+ "chapter2",
+ "chapter3"
+ ],
"end":"2013-06-13T04:30",
"tabs":[
{
@@ -54,93 +54,105 @@
"advertised_start":"Fall 2013",
"display_name":"The Ancient Greek Hero"
},
- "update_version":{ "$oid" : "1d00000000000000dddd0000" },
- "previous_version":{ "$oid" : "1d00000000000000dddd1111" },
- "edited_by":"testassist@edx.org",
- "edited_on":{
- "$date":1364483713238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd0000" },
+ "previous_version":{ "$oid" : "1d00000000000000dddd1111" },
+ "edited_by":"testassist@edx.org",
+ "edited_on":{
+ "$date":1364483713238
+ }
}
},
"chapter1":{
- "children":[
-
- ],
"category":"chapter",
"definition":"chapter12345_1",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"display_name":"Hercules"
},
- "update_version":{ "$oid" : "1d00000000000000dddd0000" },
- "previous_version":null,
- "edited_by":"testassist@edx.org",
- "edited_on":{
- "$date":1364483713238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd0000" },
+ "previous_version":null,
+ "edited_by":"testassist@edx.org",
+ "edited_on":{
+ "$date":1364483713238
+ }
}
},
"chapter2":{
- "children":[
-
- ],
"category":"chapter",
"definition":"chapter12345_2",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"display_name":"Hera heckles Hercules"
},
- "update_version":{ "$oid" : "1d00000000000000dddd0000" },
- "previous_version":null,
- "edited_by":"testassist@edx.org",
- "edited_on":{
- "$date":1364483713238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd0000" },
+ "previous_version":null,
+ "edited_by":"testassist@edx.org",
+ "edited_on":{
+ "$date":1364483713238
+ }
}
},
"chapter3":{
- "children":[
- "problem1",
- "problem3_2"
- ],
"category":"chapter",
"definition":"chapter12345_3",
- "metadata":{
+ "fields":{
+ "children":[
+ "problem1",
+ "problem3_2"
+ ],
"display_name":"Hera cuckolds Zeus"
},
- "update_version":{ "$oid" : "1d00000000000000dddd0000" },
- "previous_version":null,
- "edited_by":"testassist@edx.org",
- "edited_on":{
- "$date":1364483713238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd0000" },
+ "previous_version":null,
+ "edited_by":"testassist@edx.org",
+ "edited_on":{
+ "$date":1364483713238
+ }
}
},
"problem1":{
- "children":[
-
- ],
"category":"problem",
"definition":"problem12345_3_1",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"display_name":"Problem 3.1",
"graceperiod":"4 hours 0 minutes 0 seconds"
},
- "update_version":{ "$oid" : "1d00000000000000dddd0000" },
- "previous_version":null,
- "edited_by":"testassist@edx.org",
- "edited_on":{
- "$date":1364483713238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd0000" },
+ "previous_version":null,
+ "edited_by":"testassist@edx.org",
+ "edited_on":{
+ "$date":1364483713238
+ }
}
},
"problem3_2":{
- "children":[
-
- ],
"category":"problem",
"definition":"problem12345_3_2",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"display_name":"Problem 3.2"
},
- "update_version":{ "$oid" : "1d00000000000000dddd0000" },
- "previous_version":null,
- "edited_by":"testassist@edx.org",
- "edited_on":{
- "$date":1364483713238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd0000" },
+ "previous_version":null,
+ "edited_by":"testassist@edx.org",
+ "edited_on":{
+ "$date":1364483713238
+ }
}
}
}
@@ -156,12 +168,12 @@
},
"blocks":{
"head12345":{
- "children":[
-
- ],
"category":"course",
"definition":"head12345_11",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"end":"2013-04-13T04:30",
"tabs":[
{
@@ -198,11 +210,13 @@
"advertised_start":null,
"display_name":"The Ancient Greek Hero"
},
- "update_version":{ "$oid" : "1d00000000000000dddd1111" },
- "previous_version":{ "$oid" : "1d00000000000000dddd3333" },
- "edited_by":"testassist@edx.org",
- "edited_on":{
- "$date":1364481713238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd1111" },
+ "previous_version":{ "$oid" : "1d00000000000000dddd3333" },
+ "edited_by":"testassist@edx.org",
+ "edited_on":{
+ "$date":1364481713238
+ }
}
}
}
@@ -218,12 +232,12 @@
},
"blocks":{
"head12345":{
- "children":[
-
- ],
"category":"course",
"definition":"head12345_10",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"end":null,
"tabs":[
{
@@ -250,11 +264,13 @@
"advertised_start":null,
"display_name":"The Ancient Greek Hero"
},
- "update_version":{ "$oid" : "1d00000000000000dddd3333" },
- "previous_version":null,
- "edited_by":"test@edx.org",
- "edited_on":{
- "$date":1364473713238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd3333" },
+ "previous_version":null,
+ "edited_by":"test@edx.org",
+ "edited_on":{
+ "$date":1364473713238
+ }
}
}
}
@@ -270,12 +286,12 @@
},
"blocks":{
"head23456":{
- "children":[
-
- ],
"category":"course",
"definition":"head23456_1",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"end":null,
"tabs":[
{
@@ -302,11 +318,13 @@
"advertised_start":null,
"display_name":"The most wonderful course"
},
- "update_version":{ "$oid" : "1d00000000000000dddd2222" },
- "previous_version":{ "$oid" : "1d00000000000000dddd4444" },
- "edited_by":"test@edx.org",
- "edited_on":{
- "$date":1364481313238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd2222" },
+ "previous_version":{ "$oid" : "1d00000000000000dddd4444" },
+ "edited_by":"test@edx.org",
+ "edited_on":{
+ "$date":1364481313238
+ }
}
}
@@ -323,12 +341,12 @@
},
"blocks":{
"head23456":{
- "children":[
-
- ],
"category":"course",
"definition":"head23456_0",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"end":null,
"tabs":[
{
@@ -355,11 +373,13 @@
"advertised_start":null,
"display_name":"A wonderful course"
},
- "update_version":{ "$oid" : "1d00000000000000dddd4444" },
- "previous_version":null,
- "edited_by":"test@edx.org",
- "edited_on":{
- "$date":1364480313238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd4444" },
+ "previous_version":null,
+ "edited_by":"test@edx.org",
+ "edited_on":{
+ "$date":1364480313238
+ }
}
}
}
@@ -375,12 +395,12 @@
},
"blocks":{
"head23456":{
- "children":[
-
- ],
"category":"course",
"definition":"head23456_1",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"end":null,
"tabs":[
{
@@ -407,11 +427,13 @@
"advertised_start":null,
"display_name":"The most wonderful course"
},
- "update_version":{ "$oid" : "1d00000000000000eeee0000" },
- "previous_version":null,
- "edited_by":"test@edx.org",
- "edited_on":{
- "$date":1364481333238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000eeee0000" },
+ "previous_version":null,
+ "edited_by":"test@edx.org",
+ "edited_on":{
+ "$date":1364481333238
+ }
}
}
}
@@ -427,12 +449,12 @@
},
"blocks":{
"head345679":{
- "children":[
-
- ],
"category":"course",
"definition":"head345679_1",
- "metadata":{
+ "fields":{
+ "children":[
+
+ ],
"end":null,
"tabs":[
{
@@ -459,11 +481,13 @@
"advertised_start":null,
"display_name":"Yet another contender"
},
- "update_version":{ "$oid" : "1d00000000000000dddd5555" },
- "previous_version":null,
- "edited_by":"test@guestx.edu",
- "edited_on":{
- "$date":1364491313238
+ "edit_info": {
+ "update_version":{ "$oid" : "1d00000000000000dddd5555" },
+ "previous_version":null,
+ "edited_by":"test@guestx.edu",
+ "edited_on":{
+ "$date":1364491313238
+ }
}
}
}