Commit 9a039e93 by Mathew Peterson

Adding auto_publishing to split_draft

Added update_item, create_item, create_child to split_draft.py

Cleaned up TODOs in test_mixed_modulestore.py

Unit test for auto-publish

LMS-11017

Added tests to test_mixed_modulestore.py for has_changes and added black_list to _auto_publish

Moved DIRECT_ONLY_CATEGORIES to draft_and_publish.py

Deleted test_split_draft_modulestore.py

Added _auto_publish to create course

Publish cleanups for update_version, source_version

Changed has_changes to use source_version

Removing mixed test that no longer makes sense in auto_publish world

Addressed TODOs in test_branch_setting
parent c770f6e7
...@@ -9,10 +9,11 @@ from django.test.client import Client ...@@ -9,10 +9,11 @@ from django.test.client import Client
from django.contrib.auth.models import User from django.contrib.auth.models import User
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import PublishState, ModuleStoreEnum, mongo from xmodule.modulestore import PublishState, ModuleStoreEnum
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from student.models import Registration from student.models import Registration
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
...@@ -262,21 +263,13 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -262,21 +263,13 @@ class CourseTestCase(ModuleStoreTestCase):
self.store.compute_publish_state(course2_item) self.store.compute_publish_state(course2_item)
) )
except AssertionError: except AssertionError:
# TODO LMS-11017 "Studio auto-publish course-wide features and settings"
# Temporary hack until autopublish implemented - right now, because we call
# update_item within create_course to set the wiki & other course-wide settings,
# the publish version does not necessarily equal the draft version in split.
# So if either item is in Split, just continue on
if not isinstance(course1_item.runtime.modulestore, SplitMongoModuleStore) and \
not isinstance(course2_item.runtime.modulestore, SplitMongoModuleStore):
# old mongo calls things draft if draft exists even if it's != published; so, do more work
c1_state = self.compute_real_state(course1_item) c1_state = self.compute_real_state(course1_item)
c2_state = self.compute_real_state(course2_item) c2_state = self.compute_real_state(course2_item)
self.assertEqual( self.assertEqual(
c1_state, c1_state,
c2_state, c2_state,
"Course item {} in state {} != course item {} in state {}".format( "Publish states not equal: course item {} in state {} != course item {} in state {}".format(
course1_item, c1_state, course2_item, c2_state course1_item.location, c1_state, course2_item.location, c2_state
) )
) )
...@@ -351,7 +344,7 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -351,7 +344,7 @@ class CourseTestCase(ModuleStoreTestCase):
return supposed_state return supposed_state
# published == item in all respects, so return public # published == item in all respects, so return public
return PublishState.public return PublishState.public
elif supposed_state == PublishState.public and item.location.category in mongo.base.DIRECT_ONLY_CATEGORIES: elif supposed_state == PublishState.public and item.location.category in DIRECT_ONLY_CATEGORIES:
if not all([ if not all([
self.store.has_item(child_loc, revision=ModuleStoreEnum.RevisionOption.draft_only) self.store.has_item(child_loc, revision=ModuleStoreEnum.RevisionOption.draft_only)
for child_loc in item.children for child_loc in item.children
......
...@@ -23,7 +23,7 @@ import xmodule ...@@ -23,7 +23,7 @@ import xmodule
from xmodule.tabs import StaticTab, CourseTabList from xmodule.tabs import StaticTab, CourseTabList
from xmodule.modulestore import PublishState, ModuleStoreEnum from xmodule.modulestore import PublishState, ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW
......
...@@ -4,4 +4,4 @@ Backwards compatibility for old pointers to draft module store ...@@ -4,4 +4,4 @@ Backwards compatibility for old pointers to draft module store
This modulestore has been moved to xmodule.modulestore.mongo.draft This modulestore has been moved to xmodule.modulestore.mongo.draft
""" """
from xmodule.modulestore.mongo.draft import DIRECT_ONLY_CATEGORIES, DraftModuleStore from xmodule.modulestore.mongo.draft import DraftModuleStore
...@@ -7,6 +7,8 @@ from abc import ABCMeta, abstractmethod ...@@ -7,6 +7,8 @@ from abc import ABCMeta, abstractmethod
from contextlib import contextmanager from contextlib import contextmanager
from . import ModuleStoreEnum from . import ModuleStoreEnum
# Things w/ these categories should never be marked as version=DRAFT
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
class BranchSettingMixin(object): class BranchSettingMixin(object):
""" """
......
...@@ -35,7 +35,7 @@ from xblock.exceptions import InvalidScopeError ...@@ -35,7 +35,7 @@ from xblock.exceptions import InvalidScopeError
from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict
from xmodule.modulestore import ModuleStoreWriteBase, ModuleStoreEnum from xmodule.modulestore import ModuleStoreWriteBase, ModuleStoreEnum
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, ReferentialIntegrityError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, ReferentialIntegrityError
from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
...@@ -46,10 +46,6 @@ from xmodule.exceptions import HeartbeatFailure ...@@ -46,10 +46,6 @@ from xmodule.exceptions import HeartbeatFailure
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Things w/ these categories should never be marked as version=DRAFT
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
# sort order that returns DRAFT items first # sort order that returns DRAFT items first
SORT_REVISION_FAVOR_DRAFT = ('_id.revision', pymongo.DESCENDING) SORT_REVISION_FAVOR_DRAFT = ('_id.revision', pymongo.DESCENDING)
......
...@@ -17,10 +17,10 @@ from xmodule.modulestore.exceptions import ( ...@@ -17,10 +17,10 @@ from xmodule.modulestore.exceptions import (
) )
from xmodule.modulestore.mongo.base import ( from xmodule.modulestore.mongo.base import (
MongoModuleStore, MongoRevisionKey, as_draft, as_published, MongoModuleStore, MongoRevisionKey, as_draft, as_published,
DIRECT_ONLY_CATEGORIES, SORT_REVISION_FAVOR_DRAFT SORT_REVISION_FAVOR_DRAFT
) )
from xmodule.modulestore.store_utilities import rewrite_nonportable_content_links from xmodule.modulestore.store_utilities import rewrite_nonportable_content_links
from xmodule.modulestore.draft_and_published import UnsupportedRevisionError from xmodule.modulestore.draft_and_published import UnsupportedRevisionError, DIRECT_ONLY_CATEGORIES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -153,6 +153,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -153,6 +153,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
module.edited_on = edit_info.get('edited_on') module.edited_on = edit_info.get('edited_on')
module.previous_version = edit_info.get('previous_version') module.previous_version = edit_info.get('previous_version')
module.update_version = edit_info.get('update_version') module.update_version = edit_info.get('update_version')
module.source_version = edit_info.get('source_version', None)
module.definition_locator = definition_id module.definition_locator = definition_id
# decache any pending field settings # decache any pending field settings
module.save() module.save()
......
...@@ -37,6 +37,7 @@ Representation: ...@@ -37,6 +37,7 @@ Representation:
***** 'previous_version': the guid for the structure which previously changed this xblock ***** 'previous_version': the guid for the structure which previously changed this xblock
(will be the previous value of update_version; so, may point to a structure not in this (will be the previous value of update_version; so, may point to a structure not in this
structure's history.) structure's history.)
***** 'source_version': the guid for the structure was copied/published into this block
* definition: shared content with revision history for xblock content fields * definition: shared content with revision history for xblock content fields
** '_id': definition_id (guid), ** '_id': definition_id (guid),
** 'category': xblock type id ** 'category': xblock type id
...@@ -80,8 +81,6 @@ from xmodule.modulestore.split_mongo import encode_key_for_mongo, decode_key_fro ...@@ -80,8 +81,6 @@ from xmodule.modulestore.split_mongo import encode_key_for_mongo, decode_key_fro
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#============================================================================== #==============================================================================
# Documentation is at
# https://edx-wiki.atlassian.net/wiki/display/ENG/Mongostore+Data+Structure
# #
# Known issue: # Known issue:
# Inheritance for cached kvs doesn't work on edits. Use case. # Inheritance for cached kvs doesn't work on edits. Use case.
...@@ -99,6 +98,9 @@ log = logging.getLogger(__name__) ...@@ -99,6 +98,9 @@ log = logging.getLogger(__name__)
# #
#============================================================================== #==============================================================================
# When blacklists are this, all children should be excluded
EXCLUDE_ALL = '*'
class SplitMongoModuleStore(ModuleStoreWriteBase): class SplitMongoModuleStore(ModuleStoreWriteBase):
""" """
...@@ -1040,11 +1042,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1040,11 +1042,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
else: else:
versions_dict[master_branch] = new_id versions_dict[master_branch] = new_id
else: elif definition_fields or block_fields: # pointing to existing course w/ some overrides
# just get the draft_version structure # just get the draft_version structure
draft_version = CourseLocator(version_guid=versions_dict[master_branch]) draft_version = CourseLocator(version_guid=versions_dict[master_branch])
draft_structure = self._lookup_course(draft_version)['structure'] draft_structure = self._lookup_course(draft_version)['structure']
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']
encoded_block_id = encode_key_for_mongo(draft_structure['root']) encoded_block_id = encode_key_for_mongo(draft_structure['root'])
...@@ -1358,6 +1359,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1358,6 +1359,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
destination_structure = self._lookup_course(destination_course)['structure'] destination_structure = self._lookup_course(destination_course)['structure']
destination_structure = self._version_structure(destination_structure, user_id) destination_structure = self._version_structure(destination_structure, user_id)
if blacklist != EXCLUDE_ALL:
blacklist = [shunned.block_id for shunned in blacklist or []] blacklist = [shunned.block_id for shunned in blacklist or []]
# iterate over subtree list filtering out blacklist. # iterate over subtree list filtering out blacklist.
orphans = set() orphans = set()
...@@ -1379,7 +1381,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1379,7 +1381,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# update/create the subtree and its children in destination (skipping blacklist) # update/create the subtree and its children in destination (skipping blacklist)
orphans.update( orphans.update(
self._publish_subdag( self._publish_subdag(
user_id, subtree_root.block_id, source_structure['blocks'], destination_blocks, blacklist user_id, destination_structure['_id'],
subtree_root.block_id, source_structure['blocks'], destination_blocks, blacklist
) )
) )
# remove any remaining orphans # remove any remaining orphans
...@@ -1785,7 +1788,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1785,7 +1788,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
destination_parent['fields']['children'] = destination_reordered destination_parent['fields']['children'] = destination_reordered
return orphans return orphans
def _publish_subdag(self, user_id, block_id, source_blocks, destination_blocks, blacklist): def _publish_subdag(self, user_id, destination_version, block_id, source_blocks, destination_blocks, blacklist):
""" """
Update destination_blocks for the sub-dag rooted at block_id to be like the one in Update destination_blocks for the sub-dag rooted at block_id to be like the one in
source_blocks excluding blacklist. source_blocks excluding blacklist.
...@@ -1797,29 +1800,50 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1797,29 +1800,50 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
destination_block = destination_blocks.get(encoded_block_id) destination_block = destination_blocks.get(encoded_block_id)
new_block = source_blocks[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']: # reorder children to correspond to whatever order holds for source.
# remove any which source no longer claims (put into orphans)
# add any which are being published
source_children = new_block['fields'].get('children', []) source_children = new_block['fields'].get('children', [])
for child in destination_block['fields'].get('children', []): existing_children = destination_block['fields'].get('children', [])
destination_reordered = SparseList()
for child in existing_children:
try: try:
source_children.index(child) index = source_children.index(child)
destination_reordered[index] = child
except ValueError: except ValueError:
orphans.add(child) orphans.add(child)
previous_version = new_block['edit_info']['update_version'] if blacklist != EXCLUDE_ALL:
for index, child in enumerate(source_children):
if child not in blacklist:
destination_reordered[index] = child
# the history of the published leaps between publications and only points to
# previously published versions.
previous_version = destination_block['edit_info']['update_version']
destination_block = copy.deepcopy(new_block) destination_block = copy.deepcopy(new_block)
destination_block['fields'] = self._filter_blacklist(destination_block['fields'], blacklist) destination_block['fields']['children'] = destination_reordered.compact_list()
destination_block['edit_info']['previous_version'] = previous_version destination_block['edit_info']['previous_version'] = previous_version
destination_block['edit_info']['update_version'] = destination_version
destination_block['edit_info']['edited_by'] = user_id destination_block['edit_info']['edited_by'] = user_id
else: else:
destination_block = self._new_block( destination_block = self._new_block(
user_id, new_block['category'], user_id, new_block['category'],
self._filter_blacklist(copy.copy(new_block['fields']), blacklist), self._filter_blacklist(copy.copy(new_block['fields']), blacklist),
new_block['definition'], new_block['definition'],
new_block['edit_info']['update_version'], destination_version,
raw=True raw=True
) )
# introduce new edit info field for tracing where copied/published blocks came
destination_block['edit_info']['source_version'] = new_block['edit_info']['update_version']
if blacklist != EXCLUDE_ALL:
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, destination_version, child, source_blocks, destination_blocks, blacklist
)
)
destination_blocks[encoded_block_id] = destination_block destination_blocks[encoded_block_id] = destination_block
return orphans return orphans
...@@ -1828,6 +1852,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1828,6 +1852,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Filter out blacklist from the children field in fields. Will construct a new list for children; Filter out blacklist from the children field in fields. Will construct a new list for children;
so, no need to worry about copying the children field, but it will modify fiels. so, no need to worry about copying the children field, but it will modify fiels.
""" """
if blacklist == EXCLUDE_ALL:
fields['children'] = []
else:
fields['children'] = [child for child in fields.get('children', []) if child not in blacklist] fields['children'] = [child for child in fields.get('children', []) if child not in blacklist]
return fields return fields
...@@ -1905,3 +1932,24 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1905,3 +1932,24 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Check that the db is reachable. Check that the db is reachable.
""" """
return {ModuleStoreEnum.Type.split: self.db_connection.heartbeat()} return {ModuleStoreEnum.Type.split: self.db_connection.heartbeat()}
class SparseList(list):
"""
Enable inserting items into a list in arbitrary order and then retrieving them.
"""
# taken from http://stackoverflow.com/questions/1857780/sparse-assignment-list-in-python
def __setitem__(self, index, value):
"""
Add value to the list ensuring the list is long enough to accommodate it at the given index
"""
missing = index - len(self) + 1
if missing > 0:
self.extend([None] * missing)
list.__setitem__(self, index, value)
def compact_list(self):
"""
Return as a regular lists w/ all Nones removed
"""
return [ele for ele in self if ele is not None]
...@@ -3,11 +3,10 @@ Module for the dual-branch fall-back Draft->Published Versioning ModuleStore ...@@ -3,11 +3,10 @@ Module for the dual-branch fall-back Draft->Published Versioning ModuleStore
""" """
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
from split import SplitMongoModuleStore from split import SplitMongoModuleStore, EXCLUDE_ALL
from xmodule.modulestore import ModuleStoreEnum, PublishState from xmodule.modulestore import ModuleStoreEnum, PublishState
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, UnsupportedRevisionError
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
from xmodule.modulestore.exceptions import InsufficientSpecificationError from xmodule.modulestore.exceptions import InsufficientSpecificationError
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES, UnsupportedRevisionError
class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore): class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore):
...@@ -46,9 +45,11 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -46,9 +45,11 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
Returns: a CourseDescriptor Returns: a CourseDescriptor
""" """
master_branch = kwargs.pop('master_branch', ModuleStoreEnum.BranchName.draft) master_branch = kwargs.pop('master_branch', ModuleStoreEnum.BranchName.draft)
return super(DraftVersioningModuleStore, self).create_course( item = super(DraftVersioningModuleStore, self).create_course(
org, course, run, user_id, master_branch=master_branch, **kwargs org, course, run, user_id, master_branch=master_branch, **kwargs
) )
self._auto_publish_no_children(item.location, item.location.category, user_id)
return item
def get_courses(self): def get_courses(self):
""" """
...@@ -56,6 +57,51 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -56,6 +57,51 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
""" """
return super(DraftVersioningModuleStore, self).get_courses(ModuleStoreEnum.BranchName.draft) return super(DraftVersioningModuleStore, self).get_courses(ModuleStoreEnum.BranchName.draft)
def _auto_publish_no_children(self, location, category, user_id):
"""
Publishes item if the category is DIRECT_ONLY. This assumes another method has checked that
location points to the head of the branch and ignores the version. If you call this in any
other context, you may blow away another user's changes.
NOTE: only publishes the item at location: no children get published.
"""
if location.branch == ModuleStoreEnum.BranchName.draft and category in DIRECT_ONLY_CATEGORIES:
# version_agnostic b/c of above assumption in docstring
self.publish(location.version_agnostic(), user_id, blacklist=EXCLUDE_ALL)
def update_item(self, descriptor, user_id, allow_not_found=False, force=False):
item = super(DraftVersioningModuleStore, self).update_item(
descriptor,
user_id,
allow_not_found=allow_not_found,
force=force
)
self._auto_publish_no_children(item.location, item.location.category, user_id)
return item
def create_item(
self, user_id, course_key, block_type, block_id=None,
definition_locator=None, fields=None,
force=False, continue_version=False, **kwargs
):
item = super(DraftVersioningModuleStore, self).create_item(
user_id, course_key, block_type, block_id=block_id,
definition_locator=definition_locator, fields=fields,
force=force, continue_version=continue_version, **kwargs
)
self._auto_publish_no_children(item.location, item.location.category, user_id)
return item
def create_child(
self, user_id, parent_usage_key, block_type, block_id=None,
fields=None, continue_version=False, **kwargs
):
item = super(DraftVersioningModuleStore, self).create_child(
user_id, parent_usage_key, block_type, block_id=block_id,
fields=fields, continue_version=continue_version, **kwargs
)
self._auto_publish_no_children(parent_usage_key, item.location.category, user_id)
return item
def delete_item(self, location, user_id, revision=None, **kwargs): def delete_item(self, location, user_id, revision=None, **kwargs):
""" """
Delete the given item from persistence. kwargs allow modulestore specific parameters. Delete the given item from persistence. kwargs allow modulestore specific parameters.
...@@ -86,7 +132,10 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -86,7 +132,10 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
) )
for branch in branches_to_delete: for branch in branches_to_delete:
SplitMongoModuleStore.delete_item(self, location.for_branch(branch), user_id, **kwargs) branched_location = location.for_branch(branch)
parent_loc = self.get_parent_location(branched_location)
SplitMongoModuleStore.delete_item(self, branched_location, user_id, **kwargs)
self._auto_publish_no_children(parent_loc, parent_loc.category, user_id)
def _map_revision_to_branch(self, key, revision=None): def _map_revision_to_branch(self, key, revision=None):
""" """
...@@ -158,20 +207,24 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -158,20 +207,24 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
except ItemNotFoundError: except ItemNotFoundError:
return True return True
return draft.update_version != published.update_version return draft.update_version != published.source_version
def publish(self, location, user_id, **kwargs): def publish(self, location, user_id, blacklist=None, **kwargs):
""" """
Save a current draft to the underlying modulestore. Publishes the subtree under location from the draft branch to the published branch
Returns the newly published item. Returns the newly published item.
""" """
SplitMongoModuleStore.copy( SplitMongoModuleStore.copy(
self, self,
user_id, user_id,
location.course_key.for_branch(ModuleStoreEnum.BranchName.draft), # Directly using the replace function rather than the for_branch function
# because for_branch obliterates the version_guid and will lead to missed version conflicts.
location.course_key.replace(branch=ModuleStoreEnum.BranchName.draft),
location.course_key.for_branch(ModuleStoreEnum.BranchName.published), location.course_key.for_branch(ModuleStoreEnum.BranchName.published),
[location], [location],
blacklist=blacklist
) )
return self.get_item(location.for_branch(ModuleStoreEnum.BranchName.published))
def unpublish(self, location, user_id): def unpublish(self, location, user_id):
""" """
...@@ -211,7 +264,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -211,7 +264,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
Return the version of the given database representation of a block. Return the version of the given database representation of a block.
""" """
#TODO: make this method a more generic helper #TODO: make this method a more generic helper
return block['edit_info']['update_version'] return block['edit_info'].get('source_version', block['edit_info']['update_version'])
draft_head = get_head(ModuleStoreEnum.BranchName.draft) draft_head = get_head(ModuleStoreEnum.BranchName.draft)
published_head = get_head(ModuleStoreEnum.BranchName.published) published_head = get_head(ModuleStoreEnum.BranchName.published)
......
...@@ -508,56 +508,6 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -508,56 +508,6 @@ class TestMongoModuleStore(unittest.TestCase):
finally: finally:
shutil.rmtree(root_dir) shutil.rmtree(root_dir)
def test_has_changes_direct_only(self):
"""
Tests that has_changes() returns false when a new xblock in a direct only category is checked
"""
course_location = Location('edX', 'toy', '2012_Fall', 'course', '2012_Fall')
chapter_location = Location('edX', 'toy', '2012_Fall', 'chapter', 'vertical_container')
# Create dummy direct only xblocks
self.draft_store.create_item(
self.dummy_user,
chapter_location.course_key,
chapter_location.block_type,
block_id=chapter_location.block_id
)
# Check that neither xblock has changes
self.assertFalse(self.draft_store.has_changes(course_location))
self.assertFalse(self.draft_store.has_changes(chapter_location))
def test_has_changes(self):
"""
Tests that has_changes() only returns true when changes are present
"""
location = Location('edX', 'toy', '2012_Fall', 'vertical', 'test_vertical')
# Create a dummy component to test against
self.draft_store.create_item(
self.dummy_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
# Not yet published, so changes are present
self.assertTrue(self.draft_store.has_changes(location))
# Publish and verify that there are no unpublished changes
self.draft_store.publish(location, self.dummy_user)
self.assertFalse(self.draft_store.has_changes(location))
# Change the component, then check that there now are changes
component = self.draft_store.get_item(location)
component.display_name = 'Changed Display Name'
self.draft_store.update_item(component, self.dummy_user)
self.assertTrue(self.draft_store.has_changes(location))
# Publish and verify again
self.draft_store.publish(location, self.dummy_user)
self.assertFalse(self.draft_store.has_changes(location))
def test_has_changes_missing_child(self): def test_has_changes_missing_child(self):
""" """
Tests that has_changes() returns False when a published parent points to a child that doesn't exist. Tests that has_changes() returns False when a published parent points to a child that doesn't exist.
......
"""
Test split_draft modulestore
"""
import unittest
import uuid
from xmodule.modulestore.split_mongo.split_draft import DraftVersioningModuleStore
from xmodule.modulestore import ModuleStoreEnum
from opaque_keys.edx.locator import CourseLocator
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.tests.test_split_modulestore import SplitModuleTest
# pylint: disable=W0613
def render_to_template_mock(*args):
pass
class TestDraftVersioningModuleStore(unittest.TestCase):
def setUp(self):
super(TestDraftVersioningModuleStore, self).setUp()
self.module_store = DraftVersioningModuleStore(
contentstore=None,
doc_store_config={
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore{0}'.format(uuid.uuid4().hex[:5]),
},
fs_root='',
default_class='xmodule.raw_module.RawDescriptor',
render_template=render_to_template_mock,
xblock_mixins=(InheritanceMixin, XModuleMixin),
)
self.addCleanup(self.module_store._drop_database)
SplitModuleTest.bootstrapDB(self.module_store)
def test_has_changes(self):
"""
Tests that has_changes() only returns true when changes are present
"""
draft_course = CourseLocator(
org='testx', course='GreekHero', run='run', branch=ModuleStoreEnum.BranchName.draft
)
head = draft_course.make_usage_key('course', 'head12345')
dummy_user = ModuleStoreEnum.UserID.test
# Not yet published, so changes are present
self.assertTrue(self.module_store.has_changes(head))
# Publish and verify that there are no unpublished changes
self.module_store.publish(head, dummy_user)
self.assertFalse(self.module_store.has_changes(head))
# Change the course, then check that there now are changes
course = self.module_store.get_item(head)
course.show_calculator = not course.show_calculator
self.module_store.update_item(course, dummy_user)
self.assertTrue(self.module_store.has_changes(head))
# Publish and verify again
self.module_store.publish(head, dummy_user)
self.assertFalse(self.module_store.has_changes(head))
...@@ -1687,7 +1687,12 @@ class TestPublish(SplitModuleTest): ...@@ -1687,7 +1687,12 @@ class TestPublish(SplitModuleTest):
pub_copy = modulestore().get_item(dest_course_loc.make_usage_key("", expected)) pub_copy = modulestore().get_item(dest_course_loc.make_usage_key("", expected))
# everything except previous_version & children should be the same # everything except previous_version & children should be the same
self.assertEqual(source.category, pub_copy.category) self.assertEqual(source.category, pub_copy.category)
self.assertEqual(source.update_version, pub_copy.update_version) self.assertEqual(
source.update_version, pub_copy.source_version,
u"Versions don't match for {}: {} != {}".format(
expected, source.update_version, pub_copy.update_version
)
)
self.assertEqual( self.assertEqual(
self.user_id, pub_copy.edited_by, self.user_id, pub_copy.edited_by,
"{} edited_by {} not {}".format(pub_copy.location, pub_copy.edited_by, self.user_id) "{} edited_by {} not {}".format(pub_copy.location, pub_copy.edited_by, self.user_id)
......
...@@ -15,7 +15,7 @@ import json ...@@ -15,7 +15,7 @@ import json
import os import os
from path import path from path import path
import shutil import shutil
from xmodule.modulestore.mongo.base import DIRECT_ONLY_CATEGORIES from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
DRAFT_DIR = "drafts" DRAFT_DIR = "drafts"
PUBLISHED_DIR = "published" PUBLISHED_DIR = "published"
......
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