Commit 41f88344 by Don Mitchell

Merge pull request #2167 from edx/dhm/split_block_id

Encode periods and $ in block_id map
parents 80d54afd 13a04927
...@@ -156,7 +156,7 @@ class LocMapperStore(object): ...@@ -156,7 +156,7 @@ class LocMapperStore(object):
entry = item entry = item
break break
block_id = entry['block_map'].get(self._encode_for_mongo(location.name)) block_id = entry['block_map'].get(self.encode_key_for_mongo(location.name))
if block_id is None: if block_id is None:
if add_entry_if_missing: if add_entry_if_missing:
block_id = self._add_to_block_map(location, location_id, entry['block_map']) block_id = self._add_to_block_map(location, location_id, entry['block_map'])
...@@ -231,7 +231,7 @@ class LocMapperStore(object): ...@@ -231,7 +231,7 @@ class LocMapperStore(object):
candidate['_id']['org'], candidate['_id']['org'],
candidate['_id']['course'], candidate['_id']['course'],
category, category,
self._decode_from_mongo(old_name), self.decode_key_from_mongo(old_name),
None) None)
published_locator = BlockUsageLocator( published_locator = BlockUsageLocator(
candidate['course_id'], branch=candidate['prod_branch'], block_id=block_id candidate['course_id'], branch=candidate['prod_branch'], block_id=block_id
...@@ -261,7 +261,7 @@ class LocMapperStore(object): ...@@ -261,7 +261,7 @@ class LocMapperStore(object):
# if 2 different category locations had same name, then they'll collide. Make the later # if 2 different category locations had same name, then they'll collide. Make the later
# mapped ones unique # mapped ones unique
block_id = self._verify_uniqueness(location.name, block_map) block_id = self._verify_uniqueness(location.name, block_map)
encoded_location_name = self._encode_for_mongo(location.name) encoded_location_name = self.encode_key_for_mongo(location.name)
block_map.setdefault(encoded_location_name, {})[location.category] = block_id block_map.setdefault(encoded_location_name, {})[location.category] = block_id
self.location_map.update(location_id, {'$set': {'block_map': block_map}}) self.location_map.update(location_id, {'$set': {'block_map': block_map}})
return block_id return block_id
...@@ -331,7 +331,8 @@ class LocMapperStore(object): ...@@ -331,7 +331,8 @@ class LocMapperStore(object):
return self._verify_uniqueness(name, block_map) return self._verify_uniqueness(name, block_map)
return name return name
def _encode_for_mongo(self, fieldname): @staticmethod
def encode_key_for_mongo(fieldname):
""" """
Fieldnames in mongo cannot have periods nor dollar signs. So encode them. Fieldnames in mongo cannot have periods nor dollar signs. So encode them.
:param fieldname: an atomic field name. Note, don't pass structured paths as it will flatten them :param fieldname: an atomic field name. Note, don't pass structured paths as it will flatten them
...@@ -340,9 +341,10 @@ class LocMapperStore(object): ...@@ -340,9 +341,10 @@ class LocMapperStore(object):
fieldname = fieldname.replace(char, '%{:02x}'.format(ord(char))) fieldname = fieldname.replace(char, '%{:02x}'.format(ord(char)))
return fieldname return fieldname
def _decode_from_mongo(self, fieldname): @staticmethod
def decode_key_from_mongo(fieldname):
""" """
The inverse of _encode_for_mongo The inverse of encode_key_for_mongo
:param fieldname: with period and dollar escaped :param fieldname: with period and dollar escaped
""" """
return urllib.unquote(fieldname) return urllib.unquote(fieldname)
......
...@@ -8,6 +8,7 @@ from xblock.runtime import KvsFieldData, IdReader ...@@ -8,6 +8,7 @@ from xblock.runtime import KvsFieldData, IdReader
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
from .split_mongo_kvs import SplitMongoKVS from .split_mongo_kvs import SplitMongoKVS
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xmodule.modulestore.loc_mapper_store import LocMapperStore
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -63,7 +64,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -63,7 +64,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
# Compute inheritance # Compute inheritance
modulestore.inherit_settings( modulestore.inherit_settings(
course_entry['structure'].get('blocks', {}), course_entry['structure'].get('blocks', {}),
course_entry['structure'].get('blocks', {}).get(course_entry['structure'].get('root')) course_entry['structure'].get('blocks', {}).get(
LocMapperStore.encode_key_for_mongo(course_entry['structure'].get('root'))
)
) )
self.default_class = default_class self.default_class = default_class
self.local_modules = {} self.local_modules = {}
......
...@@ -56,7 +56,7 @@ import copy ...@@ -56,7 +56,7 @@ import copy
from pytz import UTC from pytz import UTC
from xmodule.errortracker import null_error_tracker from xmodule.errortracker import null_error_tracker
from xmodule.x_module import XModuleDescriptor, prefer_xmodules from xmodule.x_module import prefer_xmodules
from xmodule.modulestore.locator import BlockUsageLocator, DefinitionLocator, CourseLocator, VersionTree, LocalId from xmodule.modulestore.locator import BlockUsageLocator, DefinitionLocator, CourseLocator, VersionTree, LocalId
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError
from xmodule.modulestore import inheritance, ModuleStoreWriteBase, Location, SPLIT_MONGO_MODULESTORE_TYPE from xmodule.modulestore import inheritance, ModuleStoreWriteBase, Location, SPLIT_MONGO_MODULESTORE_TYPE
...@@ -69,6 +69,7 @@ from xblock.runtime import Mixologist ...@@ -69,6 +69,7 @@ from xblock.runtime import Mixologist
from bson.objectid import ObjectId from bson.objectid import ObjectId
from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection
from xblock.core import XBlock from xblock.core import XBlock
from xmodule.modulestore.loc_mapper_store import LocMapperStore
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#============================================================================== #==============================================================================
...@@ -343,7 +344,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -343,7 +344,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# this error only occurs if the course does not exist # this error only occurs if the course does not exist
return False return False
return course_structure['blocks'].get(block_location.block_id) is not None return self._get_block_from_structure(course_structure, block_location.block_id) is not None
def get_item(self, location, depth=0): def get_item(self, location, depth=0):
""" """
...@@ -432,7 +433,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -432,7 +433,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
''' '''
course = self._lookup_course(locator) course = self._lookup_course(locator)
items = self._get_parents_from_structure(locator.block_id, course['structure']) items = self._get_parents_from_structure(locator.block_id, course['structure'])
return [BlockUsageLocator(url=locator.as_course_locator(), block_id=parent_id) return [BlockUsageLocator(
url=locator.as_course_locator(),
block_id=LocMapperStore.decode_key_from_mongo(parent_id),
)
for parent_id in items] for parent_id in items]
def get_orphans(self, package_id, detached_categories, branch): def get_orphans(self, package_id, detached_categories, branch):
...@@ -442,12 +446,12 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -442,12 +446,12 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
:param package_id: :param package_id:
""" """
course = self._lookup_course(CourseLocator(package_id=package_id, branch=branch)) course = self._lookup_course(CourseLocator(package_id=package_id, branch=branch))
items = set(course['structure']['blocks'].keys()) items = {LocMapperStore.decode_key_from_mongo(block_id) for block_id in course['structure']['blocks'].keys()}
items.remove(course['structure']['root']) items.remove(course['structure']['root'])
for block_id, block_data in course['structure']['blocks'].iteritems(): for block_id, block_data in course['structure']['blocks'].iteritems():
items.difference_update(block_data.get('fields', {}).get('children', [])) items.difference_update(block_data.get('fields', {}).get('children', []))
if block_data['category'] in detached_categories: if block_data['category'] in detached_categories:
items.discard(block_id) items.discard(LocMapperStore.decode_key_from_mongo(block_id))
return [ return [
BlockUsageLocator(package_id=package_id, branch=branch, block_id=block_id) BlockUsageLocator(package_id=package_id, branch=branch, block_id=block_id)
for block_id in items for block_id in items
...@@ -554,21 +558,22 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -554,21 +558,22 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
update_version_field = 'blocks.{}.edit_info.update_version'.format(block_id) update_version_field = 'blocks.{}.edit_info.update_version'.format(block_id)
all_versions_with_block = self.db_connection.find_matching_structures({'original_version': course_struct['original_version'], all_versions_with_block = self.db_connection.find_matching_structures({'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'][block_id]['edit_info']['update_version']: block_payload = self._get_block_from_structure(version, block_id)
if version['blocks'][block_id]['edit_info'].get('previous_version') is None: if version['_id'] == block_payload['edit_info']['update_version']:
possible_roots.append(version['blocks'][block_id]['edit_info']['update_version']) if block_payload['edit_info'].get('previous_version') is None:
else: possible_roots.append(block_payload['edit_info']['update_version'])
result.setdefault(version['blocks'][block_id]['edit_info']['previous_version'], set()).add( else: # map previous to {update..}
version['blocks'][block_id]['edit_info']['update_version']) result.setdefault(block_payload['edit_info']['previous_version'], set()).add(
block_payload['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'][block_id]['edit_info']['update_version'] element_to_find = self._get_block_from_structure(course_struct, block_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:
...@@ -660,6 +665,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -660,6 +665,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# {category: last_serial...} # {category: last_serial...}
# A potential confusion is if the name incorporates the parent's name, then if the child # A potential confusion is if the name incorporates the parent's name, then if the child
# moves, its id won't change and will be confusing # moves, its id won't change and will be confusing
# NOTE2: this assumes category will never contain a $ nor a period.
serial = 1 serial = 1
while category + str(serial) in course_blocks: while category + str(serial) in course_blocks:
serial += 1 serial += 1
...@@ -760,7 +766,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -760,7 +766,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# generate usage id # generate usage id
if block_id is not None: if block_id is not None:
if block_id in new_structure['blocks']: if LocMapperStore.encode_key_for_mongo(block_id) in new_structure['blocks']:
raise DuplicateItemError(block_id, self, 'structures') raise DuplicateItemError(block_id, self, 'structures')
else: else:
new_block_id = block_id new_block_id = block_id
...@@ -770,7 +776,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -770,7 +776,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
block_fields = partitioned_fields.get(Scope.settings, {}) block_fields = partitioned_fields.get(Scope.settings, {})
if Scope.children in partitioned_fields: if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children]) block_fields.update(partitioned_fields[Scope.children])
new_structure['blocks'][new_block_id] = { self._update_block_in_structure(new_structure, new_block_id, {
"category": category, "category": category,
"definition": definition_locator.definition_id, "definition": definition_locator.definition_id,
"fields": block_fields, "fields": block_fields,
...@@ -780,12 +786,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -780,12 +786,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
'previous_version': None, 'previous_version': None,
'update_version': new_id, 'update_version': new_id,
} }
} })
# if given parent, add new block as child and update parent's version # if given parent, add new block as child and update parent's version
parent = None parent = None
if isinstance(course_or_parent_locator, BlockUsageLocator) and course_or_parent_locator.block_id is not None: if isinstance(course_or_parent_locator, BlockUsageLocator) and course_or_parent_locator.block_id is not None:
parent = new_structure['blocks'][course_or_parent_locator.block_id] encoded_block_id = LocMapperStore.encode_key_for_mongo(course_or_parent_locator.block_id)
parent = new_structure['blocks'][encoded_block_id]
parent['fields'].setdefault('children', []).append(new_block_id) parent['fields'].setdefault('children', []).append(new_block_id)
if not continue_version or parent['edit_info']['update_version'] != structure['_id']: if not continue_version or parent['edit_info']['update_version'] != structure['_id']:
parent['edit_info']['edited_on'] = datetime.datetime.now(UTC) parent['edit_info']['edited_on'] = datetime.datetime.now(UTC)
...@@ -892,7 +899,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -892,7 +899,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if definition_fields or block_fields: if definition_fields or block_fields:
draft_structure = self._version_structure(draft_structure, user_id) draft_structure = self._version_structure(draft_structure, user_id)
new_id = draft_structure['_id'] new_id = draft_structure['_id']
root_block = draft_structure['blocks'][draft_structure['root']] encoded_block_id = LocMapperStore.encode_key_for_mongo(draft_structure['root'])
root_block = draft_structure['blocks'][encoded_block_id]
if block_fields is not None: if block_fields is not None:
root_block['fields'].update(block_fields) root_block['fields'].update(block_fields)
if definition_fields is not None: if definition_fields is not None:
...@@ -948,7 +956,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -948,7 +956,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
descriptor.definition_locator, is_updated = self.update_definition_from_data( descriptor.definition_locator, is_updated = self.update_definition_from_data(
descriptor.definition_locator, descriptor.get_explicitly_set_fields_by_scope(Scope.content), 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.block_id] original_entry = self._get_block_from_structure(original_structure, descriptor.location.block_id)
is_updated = is_updated or ( is_updated = is_updated or (
descriptor.has_children and original_entry['fields']['children'] != descriptor.children descriptor.has_children and original_entry['fields']['children'] != descriptor.children
) )
...@@ -962,7 +970,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -962,7 +970,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# 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.block_id] block_data = self._get_block_from_structure(new_structure, descriptor.location.block_id)
block_data["definition"] = descriptor.definition_locator.definition_id block_data["definition"] = descriptor.definition_locator.definition_id
block_data["fields"] = descriptor.get_explicitly_set_fields_by_scope(Scope.settings) block_data["fields"] = descriptor.get_explicitly_set_fields_by_scope(Scope.settings)
...@@ -1048,12 +1056,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1048,12 +1056,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
is_new = True is_new = True
is_updated = True is_updated = True
block_id = self._generate_block_id(structure_blocks, xblock.category) block_id = self._generate_block_id(structure_blocks, xblock.category)
encoded_block_id = block_id
xblock.scope_ids.usage_id.block_id = block_id xblock.scope_ids.usage_id.block_id = block_id
else: else:
is_new = False is_new = False
block_id = xblock.location.block_id encoded_block_id = LocMapperStore.encode_key_for_mongo(xblock.location.block_id)
is_updated = is_updated or ( is_updated = is_updated or (
xblock.has_children and structure_blocks[block_id]['fields']['children'] != xblock.children xblock.has_children and structure_blocks[encoded_block_id]['fields']['children'] != xblock.children
) )
children = [] children = []
...@@ -1068,13 +1077,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1068,13 +1077,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
block_fields = xblock.get_explicitly_set_fields_by_scope(Scope.settings) 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_settings(block_fields, structure_blocks[block_id]['fields']) is_updated = self._compare_settings(block_fields, structure_blocks[encoded_block_id]['fields'])
if children: if children:
block_fields['children'] = children block_fields['children'] = children
if is_updated: if is_updated:
previous_version = None if is_new else structure_blocks[block_id]['edit_info'].get('update_version') previous_version = None if is_new else structure_blocks[encoded_block_id]['edit_info'].get('update_version')
structure_blocks[block_id] = { structure_blocks[encoded_block_id] = {
"category": xblock.category, "category": xblock.category,
"definition": xblock.definition_locator.definition_id, "definition": xblock.definition_locator.definition_id,
"fields": block_fields, "fields": block_fields,
...@@ -1226,7 +1235,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1226,7 +1235,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
new_id = new_structure['_id'] new_id = new_structure['_id']
parents = self.get_parent_locations(usage_locator) parents = self.get_parent_locations(usage_locator)
for parent in parents: for parent in parents:
parent_block = new_blocks[parent.block_id] encoded_block_id = LocMapperStore.encode_key_for_mongo(parent.block_id)
parent_block = new_blocks[encoded_block_id]
parent_block['fields']['children'].remove(usage_locator.block_id) parent_block['fields']['children'].remove(usage_locator.block_id)
parent_block['edit_info']['edited_on'] = datetime.datetime.now(UTC) parent_block['edit_info']['edited_on'] = datetime.datetime.now(UTC)
parent_block['edit_info']['edited_by'] = user_id parent_block['edit_info']['edited_by'] = user_id
...@@ -1237,13 +1247,14 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1237,13 +1247,14 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
""" """
Remove the subtree rooted at block_id Remove the subtree rooted at block_id
""" """
for child in new_blocks[block_id]['fields'].get('children', []): encoded_block_id = LocMapperStore.encode_key_for_mongo(block_id)
for child in new_blocks[encoded_block_id]['fields'].get('children', []):
remove_subtree(child) remove_subtree(child)
del new_blocks[block_id] del new_blocks[encoded_block_id]
if delete_children: if delete_children:
remove_subtree(usage_locator.block_id) remove_subtree(usage_locator.block_id)
else: else:
del new_blocks[usage_locator.block_id] del new_blocks[LocMapperStore.encode_key_for_mongo(usage_locator.block_id)]
# update index if appropriate and structures # update index if appropriate and structures
self.db_connection.insert_structure(new_structure) self.db_connection.insert_structure(new_structure)
...@@ -1306,6 +1317,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1306,6 +1317,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
for child in block_fields.get('children', []): for child in block_fields.get('children', []):
try: try:
child = LocMapperStore.encode_key_for_mongo(child)
self.inherit_settings(block_map, block_map[child], inheriting_settings) self.inherit_settings(block_map, block_map[child], inheriting_settings)
except KeyError: except KeyError:
# here's where we need logic for looking up in other structures when we allow cross pointers # here's where we need logic for looking up in other structures when we allow cross pointers
...@@ -1320,16 +1332,18 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1320,16 +1332,18 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
(0 => this usage only, 1 => this usage and its children, etc...) (0 => this usage only, 1 => this usage and its children, etc...)
A depth of None returns all descendants A depth of None returns all descendants
""" """
if block_id not in block_map: encoded_block_id = LocMapperStore.encode_key_for_mongo(block_id)
if encoded_block_id not in block_map:
return descendent_map return descendent_map
if block_id not in descendent_map: if block_id not in descendent_map:
descendent_map[block_id] = block_map[block_id] descendent_map[block_id] = block_map[encoded_block_id]
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[block_id]['fields'].get('children', []): for child in descendent_map[block_id]['fields'].get('children', []):
descendent_map = self.descendants(block_map, child, depth, descendent_map) descendent_map = self.descendants(block_map, child, depth,
descendent_map)
return descendent_map return descendent_map
...@@ -1357,7 +1371,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1357,7 +1371,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
for block in original_structure['blocks'].itervalues(): for block in original_structure['blocks'].itervalues():
if 'fields' in block and 'children' in block['fields']: if 'fields' in block and 'children' in block['fields']:
block['fields']["children"] = [ block['fields']["children"] = [
block_id for block_id in block['fields']["children"] if block_id in original_structure['blocks'] block_id for block_id in block['fields']["children"]
if LocMapperStore.encode_key_for_mongo(block_id) in original_structure['blocks']
] ]
self.db_connection.update_structure(original_structure) self.db_connection.update_structure(original_structure)
# clear cache again b/c inheritance may be wrong over orphans # clear cache again b/c inheritance may be wrong over orphans
...@@ -1511,8 +1526,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1511,8 +1526,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
""" """
new_id = ObjectId() new_id = ObjectId()
if root_category is not None: if root_category is not None:
encoded_root = LocMapperStore.encode_key_for_mongo(root_block_id)
blocks = { blocks = {
root_block_id: self._new_block(user_id, root_category, block_fields, definition_id, new_id) encoded_root: self._new_block(
user_id, root_category, block_fields, definition_id, new_id
)
} }
else: else:
blocks = {} blocks = {}
...@@ -1528,7 +1546,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1528,7 +1546,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
def _get_parents_from_structure(self, block_id, structure): def _get_parents_from_structure(self, block_id, structure):
""" """
Given a structure, find all of block_id's parents in that structure Given a structure, find all of block_id's parents in that structure. Note returns
the encoded format for parent
""" """
items = [] items = []
for parent_id, value in structure['blocks'].iteritems(): for parent_id, value in structure['blocks'].iteritems():
...@@ -1566,8 +1585,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1566,8 +1585,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Return any newly discovered orphans (as a set) Return any newly discovered orphans (as a set)
""" """
orphans = set() orphans = set()
destination_block = destination_blocks.get(block_id) encoded_block_id = LocMapperStore.encode_key_for_mongo(block_id)
new_block = source_blocks[block_id] destination_block = destination_blocks.get(encoded_block_id)
new_block = source_blocks[encoded_block_id]
if destination_block: if destination_block:
if destination_block['edit_info']['update_version'] != new_block['edit_info']['update_version']: if destination_block['edit_info']['update_version'] != new_block['edit_info']['update_version']:
source_children = new_block['fields']['children'] source_children = new_block['fields']['children']
...@@ -1591,7 +1611,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1591,7 +1611,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
for child in destination_block['fields'].get('children', []): for child in destination_block['fields'].get('children', []):
if child not in blacklist: if child not in blacklist:
orphans.update(self._publish_subdag(user_id, child, source_blocks, destination_blocks, blacklist)) orphans.update(self._publish_subdag(user_id, child, source_blocks, destination_blocks, blacklist))
destination_blocks[block_id] = destination_block destination_blocks[encoded_block_id] = destination_block
return orphans return orphans
def _filter_blacklist(self, fields, blacklist): def _filter_blacklist(self, fields, blacklist):
...@@ -1607,9 +1627,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1607,9 +1627,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Delete the orphan and any of its descendants which no longer have parents. Delete the orphan and any of its descendants which no longer have parents.
""" """
if not self._get_parents_from_structure(orphan, structure): if not self._get_parents_from_structure(orphan, structure):
for child in structure['blocks'][orphan]['fields'].get('children', []): encoded_block_id = LocMapperStore.encode_key_for_mongo(orphan)
for child in structure['blocks'][encoded_block_id]['fields'].get('children', []):
self._delete_if_true_orphan(child, structure) self._delete_if_true_orphan(child, structure)
del structure['blocks'][orphan] del structure['blocks'][encoded_block_id]
def _new_block(self, user_id, category, block_fields, definition_id, new_id): def _new_block(self, user_id, category, block_fields, definition_id, new_id):
return { return {
...@@ -1623,3 +1644,17 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1623,3 +1644,17 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
'update_version': new_id 'update_version': new_id
} }
} }
def _get_block_from_structure(self, structure, block_id):
"""
Encodes the block id before retrieving it from the structure to ensure it can
be a json dict key.
"""
return structure['blocks'].get(LocMapperStore.encode_key_for_mongo(block_id))
def _update_block_in_structure(self, structure, block_id, content):
"""
Encodes the block id before accessing it in the structure to ensure it can
be a json dict key.
"""
structure['blocks'][LocMapperStore.encode_key_for_mongo(block_id)] = content
...@@ -620,6 +620,34 @@ class TestItemCrud(SplitModuleTest): ...@@ -620,6 +620,34 @@ class TestItemCrud(SplitModuleTest):
another_history = modulestore().get_definition_history_info(another_module.definition_locator) another_history = modulestore().get_definition_history_info(another_module.definition_locator)
self.assertEqual(str(another_history['previous_version']), '0d00000040000000dddd0031') self.assertEqual(str(another_history['previous_version']), '0d00000040000000dddd0031')
def test_encoded_naming(self):
"""
Check that using odd characters in block id don't break ability to add and retrieve block.
"""
parent_locator = BlockUsageLocator(package_id="contender", block_id="head345679", branch='draft')
chapter_locator = BlockUsageLocator(package_id="contender", block_id="foo.bar_-~:0", branch='draft')
modulestore().create_item(
parent_locator, 'chapter', 'anotheruser',
block_id=chapter_locator.block_id,
fields={'display_name': 'chapter 99'},
)
# check that course version changed and course's previous is the other one
new_module = modulestore().get_item(chapter_locator)
self.assertEqual(new_module.location.block_id, "foo.bar_-~:0") # hardcode to ensure BUL init didn't change
# now try making that a parent of something
new_payload = "<problem>empty</problem>"
problem_locator = BlockUsageLocator(package_id="contender", block_id="prob.bar_-~:99a", branch='draft')
modulestore().create_item(
chapter_locator, 'problem', 'anotheruser',
block_id=problem_locator.block_id,
fields={'display_name': 'chapter 99', 'data': new_payload},
)
# check that course version changed and course's previous is the other one
new_module = modulestore().get_item(problem_locator)
self.assertEqual(new_module.location.block_id, problem_locator.block_id)
chapter = modulestore().get_item(chapter_locator)
self.assertIn(problem_locator.block_id, chapter.children)
def test_create_continue_version(self): def test_create_continue_version(self):
""" """
Test create_item using the continue_version flag Test create_item using the continue_version flag
......
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