Commit 602be5e9 by Nimisha Asthagiri

Merge pull request #4532 from edx/split/branch-settings

Split/branch settings LMS-11056
parents 2c01984b 167e2b67
...@@ -100,3 +100,17 @@ class ModuleStoreDraftAndPublished(BranchSettingMixin): ...@@ -100,3 +100,17 @@ class ModuleStoreDraftAndPublished(BranchSettingMixin):
@abstractmethod @abstractmethod
def convert_to_draft(self, location, user_id): def convert_to_draft(self, location, user_id):
raise NotImplementedError raise NotImplementedError
class UnsupportedRevisionError(ValueError):
"""
This error is raised if a method is called with an unsupported revision parameter.
"""
def __init__(self, allowed_revisions=None):
if not allowed_revisions:
allowed_revisions = [
None,
ModuleStoreEnum.RevisionOption.published_only,
ModuleStoreEnum.RevisionOption.draft_only
]
super(UnsupportedRevisionError, self).__init__('revision not one of {}'.format(allowed_revisions))
...@@ -20,6 +20,7 @@ from xmodule.modulestore.mongo.base import ( ...@@ -20,6 +20,7 @@ from xmodule.modulestore.mongo.base import (
DIRECT_ONLY_CATEGORIES, SORT_REVISION_FAVOR_DRAFT DIRECT_ONLY_CATEGORIES, 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
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -97,7 +98,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -97,7 +98,7 @@ class DraftModuleStore(MongoModuleStore):
elif self.get_branch_setting() == ModuleStoreEnum.Branch.published_only: elif self.get_branch_setting() == ModuleStoreEnum.Branch.published_only:
return get_published() return get_published()
else: elif revision is None:
# could use a single query wildcarding revision and sorting by revision. would need to # could use a single query wildcarding revision and sorting by revision. would need to
# use prefix form of to_deprecated_son # use prefix form of to_deprecated_son
try: try:
...@@ -107,6 +108,9 @@ class DraftModuleStore(MongoModuleStore): ...@@ -107,6 +108,9 @@ class DraftModuleStore(MongoModuleStore):
# otherwise, fall back to the published version # otherwise, fall back to the published version
return get_published() return get_published()
else:
raise UnsupportedRevisionError()
def has_item(self, usage_key, revision=None): def has_item(self, usage_key, revision=None):
""" """
Returns True if location exists in this ModuleStore. Returns True if location exists in this ModuleStore.
...@@ -127,13 +131,17 @@ class DraftModuleStore(MongoModuleStore): ...@@ -127,13 +131,17 @@ class DraftModuleStore(MongoModuleStore):
if revision == ModuleStoreEnum.RevisionOption.draft_only: if revision == ModuleStoreEnum.RevisionOption.draft_only:
return has_draft() return has_draft()
elif revision == ModuleStoreEnum.RevisionOption.published_only \ elif (
or self.get_branch_setting() == ModuleStoreEnum.Branch.published_only: revision == ModuleStoreEnum.RevisionOption.published_only or
self.get_branch_setting() == ModuleStoreEnum.Branch.published_only
):
return has_published() return has_published()
else: elif revision is None:
key = usage_key.to_deprecated_son(prefix='_id.') key = usage_key.to_deprecated_son(prefix='_id.')
del key['_id.revision'] del key['_id.revision']
return self.collection.find(key).count() > 0 return self.collection.find(key).count() > 0
else:
raise UnsupportedRevisionError()
def delete_course(self, course_key, user_id): def delete_course(self, course_key, user_id):
""" """
...@@ -342,9 +350,11 @@ class DraftModuleStore(MongoModuleStore): ...@@ -342,9 +350,11 @@ class DraftModuleStore(MongoModuleStore):
elif revision == ModuleStoreEnum.RevisionOption.published_only \ elif revision == ModuleStoreEnum.RevisionOption.published_only \
or self.get_branch_setting() == ModuleStoreEnum.Branch.published_only: or self.get_branch_setting() == ModuleStoreEnum.Branch.published_only:
return published_items([]) return published_items([])
else: elif revision is None:
draft_items = draft_items() draft_items = draft_items()
return draft_items + published_items(draft_items) return draft_items + published_items(draft_items)
else:
raise UnsupportedRevisionError()
def convert_to_draft(self, location, user_id): def convert_to_draft(self, location, user_id):
""" """
...@@ -519,7 +529,13 @@ class DraftModuleStore(MongoModuleStore): ...@@ -519,7 +529,13 @@ class DraftModuleStore(MongoModuleStore):
elif revision is None: elif revision is None:
as_functions = [as_draft] as_functions = [as_draft]
else: else:
raise ValueError('revision not one of None, ModuleStoreEnum.RevisionOption.published_only, or ModuleStoreEnum.RevisionOption.all') raise UnsupportedRevisionError(
[
None,
ModuleStoreEnum.RevisionOption.published_only,
ModuleStoreEnum.RevisionOption.all
]
)
self._delete_subtree(location, as_functions) self._delete_subtree(location, as_functions)
def _delete_subtree(self, location, as_functions): def _delete_subtree(self, location, as_functions):
......
...@@ -125,7 +125,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -125,7 +125,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
super(SplitMongoModuleStore, self).__init__(contentstore, **kwargs) super(SplitMongoModuleStore, self).__init__(contentstore, **kwargs)
self.branch_setting_func = kwargs.pop('branch_setting_func', lambda: ModuleStoreEnum.Branch.published_only)
self.db_connection = MongoConnection(**doc_store_config) self.db_connection = MongoConnection(**doc_store_config)
self.db = self.db_connection.database self.db = self.db_connection.database
...@@ -296,14 +295,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -296,14 +295,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
''' '''
if course_locator.org and course_locator.course and course_locator.run: if course_locator.org and course_locator.course and course_locator.run:
if course_locator.branch is None: if course_locator.branch is None:
# default it based on branch_setting raise InsufficientSpecificationError(course_locator)
# NAATODO move this to your mixin
if self.branch_setting_func() == ModuleStoreEnum.Branch.draft_preferred:
course_locator = course_locator.for_branch(ModuleStoreEnum.BranchName.draft)
elif self.branch_setting_func() == ModuleStoreEnum.Branch.published_only:
course_locator = course_locator.for_branch(ModuleStoreEnum.BranchName.published)
else:
raise InsufficientSpecificationError(course_locator)
# use the course id # use the course id
index = self.db_connection.get_course_index(course_locator) index = self.db_connection.get_course_index(course_locator)
if index is None: if index is None:
...@@ -1806,8 +1798,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1806,8 +1798,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
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']: if destination_block['edit_info']['update_version'] != new_block['edit_info']['update_version']:
source_children = new_block['fields']['children'] source_children = new_block['fields'].get('children', [])
for child in destination_block['fields']['children']: for child in destination_block['fields'].get('children', []):
try: try:
source_children.index(child) source_children.index(child)
except ValueError: except ValueError:
......
...@@ -5,8 +5,9 @@ Module for the dual-branch fall-back Draft->Published Versioning ModuleStore ...@@ -5,8 +5,9 @@ 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
from xmodule.modulestore import ModuleStoreEnum, PublishState from xmodule.modulestore import ModuleStoreEnum, PublishState
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, UnsupportedRevisionError
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
from xmodule.modulestore.exceptions import InsufficientSpecificationError
class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore): class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore):
...@@ -14,7 +15,36 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -14,7 +15,36 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
A subclass of Split that supports a dual-branch fall-back versioning framework A subclass of Split that supports a dual-branch fall-back versioning framework
with a Draft branch that falls back to a Published branch. with a Draft branch that falls back to a Published branch.
""" """
def _lookup_course(self, course_locator):
"""
overrides the implementation of _lookup_course in SplitMongoModuleStore in order to
use the configured branch_setting in the course_locator
"""
if course_locator.org and course_locator.course and course_locator.run:
if course_locator.branch is None:
# default it based on branch_setting
branch_setting = self.get_branch_setting()
if branch_setting == ModuleStoreEnum.Branch.draft_preferred:
course_locator = course_locator.for_branch(ModuleStoreEnum.BranchName.draft)
elif branch_setting == ModuleStoreEnum.Branch.published_only:
course_locator = course_locator.for_branch(ModuleStoreEnum.BranchName.published)
else:
raise InsufficientSpecificationError(course_locator)
return super(DraftVersioningModuleStore, self)._lookup_course(course_locator)
def create_course(self, org, course, run, user_id, **kwargs): def create_course(self, org, course, run, user_id, **kwargs):
"""
Creates and returns the course.
Args:
org (str): the organization that owns the course
course (str): the name of the course
run (str): the name of the run
user_id: id of the user creating the course
kwargs: Any optional arguments understood by a subset of modulestores to customize instantiation
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( return 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
...@@ -47,7 +77,14 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -47,7 +77,14 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
elif revision is None: elif revision is None:
branches_to_delete = [ModuleStoreEnum.BranchName.draft] branches_to_delete = [ModuleStoreEnum.BranchName.draft]
else: else:
raise ValueError('revision not one of None, ModuleStoreEnum.RevisionOption.published_only, or ModuleStoreEnum.RevisionOption.all') raise UnsupportedRevisionError(
[
None,
ModuleStoreEnum.RevisionOption.published_only,
ModuleStoreEnum.RevisionOption.all
]
)
for branch in branches_to_delete: for branch in branches_to_delete:
SplitMongoModuleStore.delete_item(self, location.for_branch(branch), user_id, **kwargs) SplitMongoModuleStore.delete_item(self, location.for_branch(branch), user_id, **kwargs)
...@@ -59,8 +96,10 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -59,8 +96,10 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
return key.for_branch(ModuleStoreEnum.BranchName.published) return key.for_branch(ModuleStoreEnum.BranchName.published)
elif revision == ModuleStoreEnum.RevisionOption.draft_only: elif revision == ModuleStoreEnum.RevisionOption.draft_only:
return key.for_branch(ModuleStoreEnum.BranchName.draft) return key.for_branch(ModuleStoreEnum.BranchName.draft)
else: elif revision is None:
return key return key
else:
raise UnsupportedRevisionError()
def has_item(self, usage_key, revision=None): def has_item(self, usage_key, revision=None):
""" """
...@@ -163,30 +202,29 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -163,30 +202,29 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
PublishState.public - published exists and is the same as draft PublishState.public - published exists and is the same as draft
PublishState.private - no published version exists PublishState.private - no published version exists
""" """
# TODO figure out what to say if xblock is not from the HEAD of its branch
def get_head(branch): def get_head(branch):
course_structure = self._lookup_course(xblock.location.course_key.for_branch(branch))['structure'] course_structure = self._lookup_course(xblock.location.course_key.for_branch(branch))['structure']
return self._get_block_from_structure(course_structure, xblock.location.block_id) return self._get_block_from_structure(course_structure, xblock.location.block_id)
if xblock.location.branch == ModuleStoreEnum.BranchName.draft: def get_version(block):
try: """
other = get_head(ModuleStoreEnum.BranchName.published) Return the version of the given database representation of a block.
except ItemNotFoundError: """
return PublishState.private #TODO: make this method a more generic helper
elif xblock.location.branch == ModuleStoreEnum.BranchName.published: return block['edit_info']['update_version']
other = get_head(ModuleStoreEnum.BranchName.draft)
draft_head = get_head(ModuleStoreEnum.BranchName.draft)
published_head = get_head(ModuleStoreEnum.BranchName.published)
if not published_head:
# published version does not exist
return PublishState.private
elif get_version(draft_head) == get_version(published_head):
# published and draft versions are equal
return PublishState.public
else: else:
raise ValueError(u'{} is in a branch other than draft or published.'.format(xblock.location)) # published and draft versions differ
if not other:
if xblock.location.branch == ModuleStoreEnum.BranchName.draft:
return PublishState.private
else:
return PublishState.public
elif xblock.update_version != other['edit_info']['update_version']:
return PublishState.draft return PublishState.draft
else:
return PublishState.public
def convert_to_draft(self, location, user_id): def convert_to_draft(self, location, user_id):
""" """
......
...@@ -20,6 +20,7 @@ from django.conf import settings ...@@ -20,6 +20,7 @@ from django.conf import settings
if not settings.configured: if not settings.configured:
settings.configure() settings.configure()
from xmodule.modulestore.mixed import MixedModuleStore from xmodule.modulestore.mixed import MixedModuleStore
from xmodule.modulestore.draft_and_published import UnsupportedRevisionError
@ddt.ddt @ddt.ddt
...@@ -255,6 +256,10 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -255,6 +256,10 @@ class TestMixedModuleStore(unittest.TestCase):
)) ))
self.assertFalse(self.store.has_item(self.fake_location)) self.assertFalse(self.store.has_item(self.fake_location))
# verify that an error is raised when the revision is not valid
with self.assertRaises(UnsupportedRevisionError):
self.store.has_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
@ddt.data('draft', 'split') @ddt.data('draft', 'split')
def test_get_item(self, default_ms): def test_get_item(self, default_ms):
self.initdb(default_ms) self.initdb(default_ms)
...@@ -269,6 +274,10 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -269,6 +274,10 @@ class TestMixedModuleStore(unittest.TestCase):
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.fake_location) self.store.get_item(self.fake_location)
# verify that an error is raised when the revision is not valid
with self.assertRaises(UnsupportedRevisionError):
self.store.get_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
@ddt.data('draft', 'split') @ddt.data('draft', 'split')
def test_get_items(self, default_ms): def test_get_items(self, default_ms):
self.initdb(default_ms) self.initdb(default_ms)
...@@ -279,6 +288,13 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -279,6 +288,13 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(len(modules), 1) self.assertEqual(len(modules), 1)
self.assertEqual(modules[0].location, course_locn) self.assertEqual(modules[0].location, course_locn)
# verify that an error is raised when the revision is not valid
with self.assertRaises(UnsupportedRevisionError):
self.store.get_items(
self.course_locations[self.MONGO_COURSEID].course_key,
revision=ModuleStoreEnum.RevisionOption.draft_preferred
)
@ddt.data('draft', 'split') @ddt.data('draft', 'split')
def test_update_item(self, default_ms): def test_update_item(self, default_ms):
""" """
...@@ -366,6 +382,14 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -366,6 +382,14 @@ class TestMixedModuleStore(unittest.TestCase):
self.user_id, private_vert.location, 'html', block_id='bug_leaf' self.user_id, private_vert.location, 'html', block_id='bug_leaf'
) )
# verify that an error is raised when the revision is not valid
with self.assertRaises(UnsupportedRevisionError):
self.store.delete_item(
private_leaf.location,
self.user_id,
revision=ModuleStoreEnum.RevisionOption.draft_preferred
)
self.store.publish(private_vert.location, self.user_id) self.store.publish(private_vert.location, self.user_id)
private_leaf.display_name = 'change me' private_leaf.display_name = 'change me'
private_leaf = self.store.update_item(private_leaf, self.user_id) private_leaf = self.store.update_item(private_leaf, self.user_id)
...@@ -802,7 +826,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -802,7 +826,7 @@ class TestMixedModuleStore(unittest.TestCase):
wiki_courses wiki_courses
) )
@ddt.data('draft') @ddt.data('draft', 'split')
def test_branch_setting(self, default_ms): def test_branch_setting(self, default_ms):
""" """
Test the branch_setting context manager Test the branch_setting context manager
...@@ -810,10 +834,22 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -810,10 +834,22 @@ class TestMixedModuleStore(unittest.TestCase):
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
problem_location = self.problem_x1a_1 # TODO - Remove these lines once LMS-11017 is implemented
problem_original_name = 'Problem_x1a_1' course_location = self.course_locations[self.MONGO_COURSEID]
problem_new_name = 'New Problem Name' self.store.publish(course_location, self.user_id)
problem_original_name = 'Problem_Original'
problem = self.store.create_child(
self.user_id, self.writable_chapter_location, 'problem', 'prob_block',
fields={'display_name': problem_original_name},
)
problem_location = problem.location.version_agnostic().for_branch(None)
# TODO - Uncomment out these lines once LMS-11017 is implemented
# problem_location = self.problem_x1a_1
# problem_original_name = 'Problem_x1a_1'
course_key = problem_location.course_key course_key = problem_location.course_key
problem_new_name = 'New Problem Name'
def assertNumProblems(display_name, expected_number): def assertNumProblems(display_name, expected_number):
""" """
...@@ -837,10 +873,12 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -837,10 +873,12 @@ class TestMixedModuleStore(unittest.TestCase):
# verify Draft problem # verify Draft problem
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course_key): with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course_key):
self.assertTrue(self.store.has_item(problem_location))
assertProblemNameEquals(problem_original_name) assertProblemNameEquals(problem_original_name)
# verify Published problem doesn't exist # verify Published problem doesn't exist
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, course_key): with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, course_key):
self.assertFalse(self.store.has_item(problem_location))
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
self.store.get_item(problem_location) self.store.get_item(problem_location)
...@@ -849,6 +887,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -849,6 +887,7 @@ class TestMixedModuleStore(unittest.TestCase):
# verify Published problem # verify Published problem
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, course_key): with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, course_key):
self.assertTrue(self.store.has_item(problem_location))
assertProblemNameEquals(problem_original_name) assertProblemNameEquals(problem_original_name)
# verify Draft-preferred # verify Draft-preferred
......
...@@ -12,8 +12,11 @@ import random ...@@ -12,8 +12,11 @@ import random
from xblock.fields import Scope from xblock.fields import Scope
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import (ItemNotFoundError, VersionConflictError, from xmodule.modulestore.exceptions import (
DuplicateItemError, DuplicateCourseError) ItemNotFoundError, VersionConflictError,
DuplicateItemError, DuplicateCourseError,
InsufficientSpecificationError
)
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator, VersionTree, LocalId from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator, VersionTree, LocalId
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import XModuleMixin from xmodule.x_module import XModuleMixin
...@@ -667,7 +670,7 @@ class SplitModuleCourseTests(SplitModuleTest): ...@@ -667,7 +670,7 @@ class SplitModuleCourseTests(SplitModuleTest):
def test_get_course_negative(self): def test_get_course_negative(self):
# Now negative testing # Now negative testing
with self.assertRaises(ItemNotFoundError): with self.assertRaises(InsufficientSpecificationError):
modulestore().get_course(CourseLocator(org='edu', course='meh', run='blah')) modulestore().get_course(CourseLocator(org='edu', course='meh', run='blah'))
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
modulestore().get_course(CourseLocator(org='edu', course='nosuchthing', run="run", branch=BRANCH_NAME_DRAFT)) modulestore().get_course(CourseLocator(org='edu', course='nosuchthing', run="run", branch=BRANCH_NAME_DRAFT))
......
...@@ -122,4 +122,5 @@ class TestXMLModuleStore(unittest.TestCase): ...@@ -122,4 +122,5 @@ class TestXMLModuleStore(unittest.TestCase):
# XML store does NOT allow draft_preferred branch setting # XML store does NOT allow draft_preferred branch setting
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course_key): with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course_key):
pass # verify that the above context manager raises a ValueError
pass # pragma: no cover
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