|
|
|
|
@@ -56,7 +56,7 @@ import copy
|
|
|
|
|
from pytz import UTC
|
|
|
|
|
|
|
|
|
|
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.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError
|
|
|
|
|
from xmodule.modulestore import inheritance, ModuleStoreWriteBase, Location, SPLIT_MONGO_MODULESTORE_TYPE
|
|
|
|
|
@@ -69,6 +69,7 @@ from xblock.runtime import Mixologist
|
|
|
|
|
from bson.objectid import ObjectId
|
|
|
|
|
from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection
|
|
|
|
|
from xblock.core import XBlock
|
|
|
|
|
from xmodule.modulestore.loc_mapper_store import LocMapperStore
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
#==============================================================================
|
|
|
|
|
@@ -343,7 +344,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
# this error only occurs if the course does not exist
|
|
|
|
|
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):
|
|
|
|
|
"""
|
|
|
|
|
@@ -432,7 +433,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
'''
|
|
|
|
|
course = self._lookup_course(locator)
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
|
def get_orphans(self, package_id, detached_categories, branch):
|
|
|
|
|
@@ -442,12 +446,12 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
:param package_id:
|
|
|
|
|
"""
|
|
|
|
|
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'])
|
|
|
|
|
for block_id, block_data in course['structure']['blocks'].iteritems():
|
|
|
|
|
items.difference_update(block_data.get('fields', {}).get('children', []))
|
|
|
|
|
if block_data['category'] in detached_categories:
|
|
|
|
|
items.discard(block_id)
|
|
|
|
|
items.discard(LocMapperStore.decode_key_from_mongo(block_id))
|
|
|
|
|
return [
|
|
|
|
|
BlockUsageLocator(package_id=package_id, branch=branch, block_id=block_id)
|
|
|
|
|
for block_id in items
|
|
|
|
|
@@ -554,21 +558,22 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
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'],
|
|
|
|
|
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 = []
|
|
|
|
|
result = {}
|
|
|
|
|
for version in all_versions_with_block:
|
|
|
|
|
if version['_id'] == version['blocks'][block_id]['edit_info']['update_version']:
|
|
|
|
|
if version['blocks'][block_id]['edit_info'].get('previous_version') is None:
|
|
|
|
|
possible_roots.append(version['blocks'][block_id]['edit_info']['update_version'])
|
|
|
|
|
else:
|
|
|
|
|
result.setdefault(version['blocks'][block_id]['edit_info']['previous_version'], set()).add(
|
|
|
|
|
version['blocks'][block_id]['edit_info']['update_version'])
|
|
|
|
|
block_payload = self._get_block_from_structure(version, block_id)
|
|
|
|
|
if version['_id'] == block_payload['edit_info']['update_version']:
|
|
|
|
|
if block_payload['edit_info'].get('previous_version') is None:
|
|
|
|
|
possible_roots.append(block_payload['edit_info']['update_version'])
|
|
|
|
|
else: # map previous to {update..}
|
|
|
|
|
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.
|
|
|
|
|
if len(possible_roots) > 1:
|
|
|
|
|
# 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:
|
|
|
|
|
possible_roots = [element_to_find]
|
|
|
|
|
for possibility in possible_roots:
|
|
|
|
|
@@ -660,6 +665,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
# {category: last_serial...}
|
|
|
|
|
# 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
|
|
|
|
|
# NOTE2: this assumes category will never contain a $ nor a period.
|
|
|
|
|
serial = 1
|
|
|
|
|
while category + str(serial) in course_blocks:
|
|
|
|
|
serial += 1
|
|
|
|
|
@@ -760,7 +766,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
|
|
|
|
|
# generate usage id
|
|
|
|
|
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')
|
|
|
|
|
else:
|
|
|
|
|
new_block_id = block_id
|
|
|
|
|
@@ -770,7 +776,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
block_fields = partitioned_fields.get(Scope.settings, {})
|
|
|
|
|
if Scope.children in partitioned_fields:
|
|
|
|
|
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,
|
|
|
|
|
"definition": definition_locator.definition_id,
|
|
|
|
|
"fields": block_fields,
|
|
|
|
|
@@ -780,12 +786,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
'previous_version': None,
|
|
|
|
|
'update_version': new_id,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# if given parent, add new block as child and update parent's version
|
|
|
|
|
parent = 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)
|
|
|
|
|
if not continue_version or parent['edit_info']['update_version'] != structure['_id']:
|
|
|
|
|
parent['edit_info']['edited_on'] = datetime.datetime.now(UTC)
|
|
|
|
|
@@ -892,7 +899,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
if definition_fields or block_fields:
|
|
|
|
|
draft_structure = self._version_structure(draft_structure, user_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:
|
|
|
|
|
root_block['fields'].update(block_fields)
|
|
|
|
|
if definition_fields is not None:
|
|
|
|
|
@@ -948,7 +956,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
descriptor.definition_locator, is_updated = self.update_definition_from_data(
|
|
|
|
|
descriptor.definition_locator, descriptor.get_explicitly_set_fields_by_scope(Scope.content), user_id)
|
|
|
|
|
# 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 (
|
|
|
|
|
descriptor.has_children and original_entry['fields']['children'] != descriptor.children
|
|
|
|
|
)
|
|
|
|
|
@@ -962,7 +970,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
# if updated, rev the structure
|
|
|
|
|
if is_updated:
|
|
|
|
|
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["fields"] = descriptor.get_explicitly_set_fields_by_scope(Scope.settings)
|
|
|
|
|
@@ -1048,12 +1056,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
is_new = True
|
|
|
|
|
is_updated = True
|
|
|
|
|
block_id = self._generate_block_id(structure_blocks, xblock.category)
|
|
|
|
|
encoded_block_id = block_id
|
|
|
|
|
xblock.scope_ids.usage_id.block_id = block_id
|
|
|
|
|
else:
|
|
|
|
|
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 (
|
|
|
|
|
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 = []
|
|
|
|
|
@@ -1068,13 +1077,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
|
|
|
|
|
block_fields = xblock.get_explicitly_set_fields_by_scope(Scope.settings)
|
|
|
|
|
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:
|
|
|
|
|
block_fields['children'] = children
|
|
|
|
|
|
|
|
|
|
if is_updated:
|
|
|
|
|
previous_version = None if is_new else structure_blocks[block_id]['edit_info'].get('update_version')
|
|
|
|
|
structure_blocks[block_id] = {
|
|
|
|
|
previous_version = None if is_new else structure_blocks[encoded_block_id]['edit_info'].get('update_version')
|
|
|
|
|
structure_blocks[encoded_block_id] = {
|
|
|
|
|
"category": xblock.category,
|
|
|
|
|
"definition": xblock.definition_locator.definition_id,
|
|
|
|
|
"fields": block_fields,
|
|
|
|
|
@@ -1226,7 +1235,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
new_id = new_structure['_id']
|
|
|
|
|
parents = self.get_parent_locations(usage_locator)
|
|
|
|
|
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['edit_info']['edited_on'] = datetime.datetime.now(UTC)
|
|
|
|
|
parent_block['edit_info']['edited_by'] = user_id
|
|
|
|
|
@@ -1237,13 +1247,14 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
"""
|
|
|
|
|
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)
|
|
|
|
|
del new_blocks[block_id]
|
|
|
|
|
del new_blocks[encoded_block_id]
|
|
|
|
|
if delete_children:
|
|
|
|
|
remove_subtree(usage_locator.block_id)
|
|
|
|
|
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
|
|
|
|
|
self.db_connection.insert_structure(new_structure)
|
|
|
|
|
@@ -1306,6 +1317,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
|
|
|
|
|
for child in block_fields.get('children', []):
|
|
|
|
|
try:
|
|
|
|
|
child = LocMapperStore.encode_key_for_mongo(child)
|
|
|
|
|
self.inherit_settings(block_map, block_map[child], inheriting_settings)
|
|
|
|
|
except KeyError:
|
|
|
|
|
# here's where we need logic for looking up in other structures when we allow cross pointers
|
|
|
|
|
@@ -1320,16 +1332,18 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
(0 => this usage only, 1 => this usage and its children, etc...)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
depth = depth - 1 if depth is not None else None
|
|
|
|
|
for child in block_map[block_id]['fields'].get('children', []):
|
|
|
|
|
descendent_map = self.descendants(block_map, child, depth, descendent_map)
|
|
|
|
|
for child in descendent_map[block_id]['fields'].get('children', []):
|
|
|
|
|
descendent_map = self.descendants(block_map, child, depth,
|
|
|
|
|
descendent_map)
|
|
|
|
|
|
|
|
|
|
return descendent_map
|
|
|
|
|
|
|
|
|
|
@@ -1357,7 +1371,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
for block in original_structure['blocks'].itervalues():
|
|
|
|
|
if 'fields' in block and 'children' in block['fields']:
|
|
|
|
|
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)
|
|
|
|
|
# clear cache again b/c inheritance may be wrong over orphans
|
|
|
|
|
@@ -1511,8 +1526,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
"""
|
|
|
|
|
new_id = ObjectId()
|
|
|
|
|
if root_category is not None:
|
|
|
|
|
encoded_root = LocMapperStore.encode_key_for_mongo(root_block_id)
|
|
|
|
|
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:
|
|
|
|
|
blocks = {}
|
|
|
|
|
@@ -1528,7 +1546,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
|
|
|
|
|
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 = []
|
|
|
|
|
for parent_id, value in structure['blocks'].iteritems():
|
|
|
|
|
@@ -1566,8 +1585,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
Return any newly discovered orphans (as a set)
|
|
|
|
|
"""
|
|
|
|
|
orphans = set()
|
|
|
|
|
destination_block = destination_blocks.get(block_id)
|
|
|
|
|
new_block = source_blocks[block_id]
|
|
|
|
|
encoded_block_id = LocMapperStore.encode_key_for_mongo(block_id)
|
|
|
|
|
destination_block = destination_blocks.get(encoded_block_id)
|
|
|
|
|
new_block = source_blocks[encoded_block_id]
|
|
|
|
|
if destination_block:
|
|
|
|
|
if destination_block['edit_info']['update_version'] != new_block['edit_info']['update_version']:
|
|
|
|
|
source_children = new_block['fields']['children']
|
|
|
|
|
@@ -1591,7 +1611,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
for child in destination_block['fields'].get('children', []):
|
|
|
|
|
if child not in 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
|
|
|
|
|
|
|
|
|
|
def _filter_blacklist(self, fields, blacklist):
|
|
|
|
|
@@ -1607,9 +1627,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
Delete the orphan and any of its descendants which no longer have parents.
|
|
|
|
|
"""
|
|
|
|
|
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)
|
|
|
|
|
del structure['blocks'][orphan]
|
|
|
|
|
del structure['blocks'][encoded_block_id]
|
|
|
|
|
|
|
|
|
|
def _new_block(self, user_id, category, block_fields, definition_id, new_id):
|
|
|
|
|
return {
|
|
|
|
|
@@ -1623,3 +1644,17 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
|
|
|
|
'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
|
|
|
|
|
|