Commit 7d43e399 by Nimisha Asthagiri Committed by Brian Beggs

Fix sequence navigation issues for CCX courses

MA-2258
parent 0173642f
...@@ -38,6 +38,11 @@ new_contract('XBlock', XBlock) ...@@ -38,6 +38,11 @@ new_contract('XBlock', XBlock)
LIBRARY_ROOT = 'library.xml' LIBRARY_ROOT = 'library.xml'
COURSE_ROOT = 'course.xml' COURSE_ROOT = 'course.xml'
# List of names of computed fields on xmodules that are of type usage keys.
# This list can be used to determine which fields need to be stripped of
# extraneous usage key data when entering/exiting modulestores.
XMODULE_FIELDS_WITH_USAGE_KEYS = ['location', 'parent']
class ModuleStoreEnum(object): class ModuleStoreEnum(object):
""" """
......
...@@ -17,8 +17,7 @@ from opaque_keys.edx.locator import LibraryLocator ...@@ -17,8 +17,7 @@ from opaque_keys.edx.locator import LibraryLocator
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.assetstore import AssetMetadata from xmodule.assetstore import AssetMetadata
from . import ModuleStoreWriteBase from . import ModuleStoreWriteBase, ModuleStoreEnum, XMODULE_FIELDS_WITH_USAGE_KEYS
from . import ModuleStoreEnum
from .exceptions import ItemNotFoundError, DuplicateCourseError from .exceptions import ItemNotFoundError, DuplicateCourseError
from .draft_and_published import ModuleStoreDraftAndPublished from .draft_and_published import ModuleStoreDraftAndPublished
from .split_migrator import SplitMigrator from .split_migrator import SplitMigrator
...@@ -67,8 +66,9 @@ def strip_key(func): ...@@ -67,8 +66,9 @@ def strip_key(func):
retval = retval.version_agnostic() retval = retval.version_agnostic()
if rem_branch and hasattr(retval, 'for_branch'): if rem_branch and hasattr(retval, 'for_branch'):
retval = retval.for_branch(None) retval = retval.for_branch(None)
if hasattr(retval, 'location'): for field_name in XMODULE_FIELDS_WITH_USAGE_KEYS:
retval.location = strip_key_func(retval.location) if hasattr(retval, field_name):
setattr(retval, field_name, strip_key_func(getattr(retval, field_name)))
return retval return retval
# function for stripping both, collection of, and individual, values # function for stripping both, collection of, and individual, values
......
...@@ -127,6 +127,8 @@ class MongoKeyValueStore(InheritanceKeyValueStore): ...@@ -127,6 +127,8 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
def set(self, key, value): def set(self, key, value):
if key.scope == Scope.children: if key.scope == Scope.children:
self._children = value self._children = value
elif key.scope == Scope.parent:
self._parent = value
elif key.scope == Scope.settings: elif key.scope == Scope.settings:
self._metadata[key.field_name] = value self._metadata[key.field_name] = value
elif key.scope == Scope.content: elif key.scope == Scope.content:
......
...@@ -767,15 +767,15 @@ class TestMongoKeyValueStore(unittest.TestCase): ...@@ -767,15 +767,15 @@ class TestMongoKeyValueStore(unittest.TestCase):
def test_write(self): def test_write(self):
yield (self._check_write, KeyValueStore.Key(Scope.content, None, None, 'foo'), 'new_data') yield (self._check_write, KeyValueStore.Key(Scope.content, None, None, 'foo'), 'new_data')
yield (self._check_write, KeyValueStore.Key(Scope.children, None, None, 'children'), []) yield (self._check_write, KeyValueStore.Key(Scope.children, None, None, 'children'), [])
yield (self._check_write, KeyValueStore.Key(Scope.children, None, None, 'parent'), None)
yield (self._check_write, KeyValueStore.Key(Scope.settings, None, None, 'meta'), 'new_settings') yield (self._check_write, KeyValueStore.Key(Scope.settings, None, None, 'meta'), 'new_settings')
# write Scope.parent raises InvalidScope, which is covered in test_write_invalid_scope
def test_write_non_dict_data(self): def test_write_non_dict_data(self):
self.kvs = MongoKeyValueStore('xml_data', self.parent, self.children, self.metadata) self.kvs = MongoKeyValueStore('xml_data', self.parent, self.children, self.metadata)
self._check_write(KeyValueStore.Key(Scope.content, None, None, 'data'), 'new_data') self._check_write(KeyValueStore.Key(Scope.content, None, None, 'data'), 'new_data')
def test_write_invalid_scope(self): def test_write_invalid_scope(self):
for scope in (Scope.preferences, Scope.user_info, Scope.user_state, Scope.parent): for scope in (Scope.preferences, Scope.user_info, Scope.user_state):
with assert_raises(InvalidScopeError): with assert_raises(InvalidScopeError):
self.kvs.set(KeyValueStore.Key(scope, None, None, 'foo'), 'new_value') self.kvs.set(KeyValueStore.Key(scope, None, None, 'foo'), 'new_value')
......
...@@ -13,6 +13,7 @@ from contextlib import contextmanager ...@@ -13,6 +13,7 @@ from contextlib import contextmanager
from functools import partial from functools import partial
from ccx_keys.locator import CCXLocator, CCXBlockUsageLocator from ccx_keys.locator import CCXLocator, CCXBlockUsageLocator
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from xmodule.modulestore import XMODULE_FIELDS_WITH_USAGE_KEYS
def strip_ccx(val): def strip_ccx(val):
...@@ -28,8 +29,11 @@ def strip_ccx(val): ...@@ -28,8 +29,11 @@ def strip_ccx(val):
elif isinstance(retval, CCXBlockUsageLocator): elif isinstance(retval, CCXBlockUsageLocator):
ccx_id = retval.course_key.ccx ccx_id = retval.course_key.ccx
retval = retval.to_block_locator() retval = retval.to_block_locator()
elif hasattr(retval, 'location'): else:
retval.location, ccx_id = strip_ccx(retval.location) for field_name in XMODULE_FIELDS_WITH_USAGE_KEYS:
if hasattr(retval, field_name):
stripped_field_value, ccx_id = strip_ccx(getattr(retval, field_name))
setattr(retval, field_name, stripped_field_value)
return retval, ccx_id return retval, ccx_id
...@@ -43,8 +47,9 @@ def restore_ccx(val, ccx_id): ...@@ -43,8 +47,9 @@ def restore_ccx(val, ccx_id):
elif isinstance(val, BlockUsageLocator): elif isinstance(val, BlockUsageLocator):
ccx_key = restore_ccx(val.course_key, ccx_id) ccx_key = restore_ccx(val.course_key, ccx_id)
val = CCXBlockUsageLocator(ccx_key, val.block_type, val.block_id) val = CCXBlockUsageLocator(ccx_key, val.block_type, val.block_id)
if hasattr(val, 'location'): for field_name in XMODULE_FIELDS_WITH_USAGE_KEYS:
val.location = restore_ccx(val.location, ccx_id) if hasattr(val, field_name):
setattr(val, field_name, restore_ccx(getattr(val, field_name), ccx_id))
if hasattr(val, 'children'): if hasattr(val, 'children'):
val.children = restore_ccx_collection(val.children, ccx_id) val.children = restore_ccx_collection(val.children, ccx_id)
return val return val
......
...@@ -53,6 +53,7 @@ from xmodule.modulestore.tests.django_utils import ( ...@@ -53,6 +53,7 @@ from xmodule.modulestore.tests.django_utils import (
from xmodule.modulestore.tests.factories import ( from xmodule.modulestore.tests.factories import (
CourseFactory, CourseFactory,
ItemFactory, ItemFactory,
SampleCourseFactory,
) )
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
...@@ -1049,9 +1050,9 @@ class CCXCoachTabTestCase(SharedModuleStoreTestCase): ...@@ -1049,9 +1050,9 @@ class CCXCoachTabTestCase(SharedModuleStoreTestCase):
) )
class TestStudentDashboardWithCCX(ModuleStoreTestCase): class TestStudentViewsWithCCX(ModuleStoreTestCase):
""" """
Test to ensure that the student dashboard works for users enrolled in CCX Test to ensure that the student dashboard and courseware works for users enrolled in CCX
courses. courses.
""" """
...@@ -1059,13 +1060,13 @@ class TestStudentDashboardWithCCX(ModuleStoreTestCase): ...@@ -1059,13 +1060,13 @@ class TestStudentDashboardWithCCX(ModuleStoreTestCase):
""" """
Set up courses and enrollments. Set up courses and enrollments.
""" """
super(TestStudentDashboardWithCCX, self).setUp() super(TestStudentViewsWithCCX, self).setUp()
# Create a Draft Mongo and a Split Mongo course and enroll a student user in them. # Create a Draft Mongo and a Split Mongo course and enroll a student user in them.
self.student_password = "foobar" self.student_password = "foobar"
self.student = UserFactory.create(username="test", password=self.student_password, is_staff=False) self.student = UserFactory.create(username="test", password=self.student_password, is_staff=False)
self.draft_course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo) self.draft_course = SampleCourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
self.split_course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) self.split_course = SampleCourseFactory.create(default_store=ModuleStoreEnum.Type.split)
CourseEnrollment.enroll(self.student, self.draft_course.id) CourseEnrollment.enroll(self.student, self.draft_course.id)
CourseEnrollment.enroll(self.student, self.split_course.id) CourseEnrollment.enroll(self.student, self.split_course.id)
...@@ -1078,11 +1079,16 @@ class TestStudentDashboardWithCCX(ModuleStoreTestCase): ...@@ -1078,11 +1079,16 @@ class TestStudentDashboardWithCCX(ModuleStoreTestCase):
self.ccx = CcxFactory(course_id=self.split_course.id, coach=self.coach) self.ccx = CcxFactory(course_id=self.split_course.id, coach=self.coach)
last_week = datetime.datetime.now(UTC()) - datetime.timedelta(days=7) last_week = datetime.datetime.now(UTC()) - datetime.timedelta(days=7)
override_field_for_ccx(self.ccx, self.split_course, 'start', last_week) # Required by self.ccx.has_started(). override_field_for_ccx(self.ccx, self.split_course, 'start', last_week) # Required by self.ccx.has_started().
course_key = CCXLocator.from_course_locator(self.split_course.id, self.ccx.id) self.ccx_course_key = CCXLocator.from_course_locator(self.split_course.id, self.ccx.id)
CourseEnrollment.enroll(self.student, course_key) CourseEnrollment.enroll(self.student, self.ccx_course_key)
def test_load_student_dashboard(self): def test_load_student_dashboard(self):
self.client.login(username=self.student.username, password=self.student_password) self.client.login(username=self.student.username, password=self.student_password)
response = self.client.get(reverse('dashboard')) response = self.client.get(reverse('dashboard'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue(re.search('Test CCX', response.content)) self.assertTrue(re.search('Test CCX', response.content))
def test_load_courseware(self):
self.client.login(username=self.student.username, password=self.student_password)
response = self.client.get(reverse('courseware', kwargs={'course_id': unicode(self.ccx_course_key)}))
self.assertEqual(response.status_code, 200)
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