Commit d0fcabd4 by Nimisha Asthagiri

CCX: Use Modulestore Field Overrides instead of User-specific Overrides

parent 74a36652
......@@ -50,12 +50,11 @@ class CustomCoursesForEdxOverrideProvider(FieldOverrideProvider):
return default
@classmethod
def enabled_for(cls, course):
"""CCX field overrides are enabled per-course
protect against missing attributes
def enabled_for(cls, block):
"""
CCX field overrides are enabled for CCX blocks.
"""
return getattr(course, 'enable_ccx', False)
return getattr(block.location, 'ccx', None) or getattr(block, 'enable_ccx', False)
def get_current_ccx(course_key):
......@@ -86,12 +85,9 @@ def get_override_for_ccx(ccx, block, name, default=None):
"""
overrides = _get_overrides_for_ccx(ccx)
if isinstance(block.location, CCXBlockUsageLocator):
non_ccx_key = block.location.to_block_locator()
else:
non_ccx_key = block.location
clean_ccx_key = _clean_ccx_key(block.location)
block_overrides = overrides.get(non_ccx_key, {})
block_overrides = overrides.get(clean_ccx_key, {})
if name in block_overrides:
try:
return block.fields[name].from_json(block_overrides[name])
......@@ -101,6 +97,21 @@ def get_override_for_ccx(ccx, block, name, default=None):
return default
def _clean_ccx_key(block_location):
"""
Converts the given BlockUsageKey from a CCX key to the
corresponding key for its parent course, while handling the case
where no conversion is needed. Also strips any version and
branch information from the key.
Returns the cleaned key.
"""
if isinstance(block_location, CCXBlockUsageLocator):
clean_key = block_location.to_block_locator()
else:
clean_key = block_location
return clean_key.version_agnostic().for_branch(None)
def _get_overrides_for_ccx(ccx):
"""
Returns a dictionary mapping field name to overriden value for any
......@@ -136,6 +147,7 @@ def override_field_for_ccx(ccx, block, name, value):
value_json = field.to_json(value)
serialized_value = json.dumps(value_json)
override_has_changes = False
clean_ccx_key = _clean_ccx_key(block.location)
override = get_override_for_ccx(ccx, block, name + "_instance")
if override:
......@@ -149,7 +161,7 @@ def override_field_for_ccx(ccx, block, name, value):
defaults={'value': serialized_value},
)
if created:
_get_overrides_for_ccx(ccx).setdefault(block.location, {})[name + "_id"] = override.id
_get_overrides_for_ccx(ccx).setdefault(clean_ccx_key, {})[name + "_id"] = override.id
else:
override_has_changes = serialized_value != override.value
......@@ -157,8 +169,8 @@ def override_field_for_ccx(ccx, block, name, value):
override.value = serialized_value
override.save()
_get_overrides_for_ccx(ccx).setdefault(block.location, {})[name] = value_json
_get_overrides_for_ccx(ccx).setdefault(block.location, {})[name + "_instance"] = override
_get_overrides_for_ccx(ccx).setdefault(clean_ccx_key, {})[name] = value_json
_get_overrides_for_ccx(ccx).setdefault(clean_ccx_key, {})[name + "_instance"] = override
def clear_override_for_ccx(ccx, block, name):
......@@ -185,7 +197,8 @@ def clear_ccx_field_info_from_ccx_map(ccx, block, name): # pylint: disable=inva
Remove field information from ccx overrides mapping dictionary
"""
try:
ccx_override_map = _get_overrides_for_ccx(ccx).setdefault(block.location, {})
clean_ccx_key = _clean_ccx_key(block.location)
ccx_override_map = _get_overrides_for_ccx(ccx).setdefault(clean_ccx_key, {})
ccx_override_map.pop(name)
ccx_override_map.pop(name + "_id")
ccx_override_map.pop(name + "_instance")
......
......@@ -9,6 +9,7 @@ from nose.plugins.skip import SkipTest
from courseware.views.views import progress
from courseware.field_overrides import OverrideFieldData
from courseware.testutils import FieldOverrideTestMixin
from datetime import datetime
from django.conf import settings
from django.core.cache import caches
......@@ -20,13 +21,13 @@ from request_cache.middleware import RequestCache
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xblock.core import XBlock
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, \
TEST_DATA_SPLIT_MODULESTORE, TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.factories import check_mongo_calls_range, CourseFactory, check_sum_of_calls
from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin
from ccx_keys.locator import CCXLocator
from lms.djangoapps.ccx.tests.factories import CcxFactory
from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
@attr('shard_3')
......@@ -38,8 +39,7 @@ from lms.djangoapps.ccx.tests.factories import CcxFactory
}
)
@ddt.ddt
class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
ModuleStoreTestCase):
class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseTestMixin, ModuleStoreTestCase):
"""
Base class for instrumenting SQL queries and Mongo reads for field override
providers.
......@@ -51,8 +51,6 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
# TEST_DATA must be overridden by subclasses
TEST_DATA = None
ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
def setUp(self):
"""
Create a test client, course, and user.
......@@ -172,7 +170,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
caches[cache].clear()
# Refill the metadata inheritance cache
modulestore().get_course(self.course.id, depth=None)
get_course_in_cache(self.course.id)
# We clear the request cache to simulate a new request in the LMS.
RequestCache.clear_request_cache()
......@@ -190,7 +188,8 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
@ddt.data(*itertools.product(('no_overrides', 'ccx'), range(1, 4), (True, False), (True, False)))
@ddt.unpack
@override_settings(
FIELD_OVERRIDE_PROVIDERS=(),
XBLOCK_FIELD_DATA_WRAPPERS=[],
MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[],
)
def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx):
"""
......@@ -209,7 +208,10 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
if self.MODULESTORE == TEST_DATA_MONGO_MODULESTORE and view_as_ccx:
raise SkipTest("Can't use a MongoModulestore test as a CCX course")
with self.settings(FIELD_OVERRIDE_PROVIDERS=providers[overrides]):
with self.settings(
XBLOCK_FIELD_DATA_WRAPPERS=['lms.djangoapps.courseware.field_overrides:OverrideModulestoreFieldData.wrap'],
MODULESTORE_FIELD_OVERRIDE_PROVIDERS=providers[overrides],
):
default_queries, history_queries, reads, xblocks = self.TEST_DATA[
(overrides, course_width, enable_ccx, view_as_ccx)
]
......@@ -232,24 +234,24 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of mongo queries,
# # of xblocks
# )
('no_overrides', 1, True, False): (47, 1, 6, 13),
('no_overrides', 2, True, False): (119, 16, 6, 84),
('no_overrides', 3, True, False): (399, 81, 6, 335),
('ccx', 1, True, False): (47, 1, 6, 13),
('ccx', 2, True, False): (119, 16, 6, 84),
('ccx', 3, True, False): (399, 81, 6, 335),
('ccx', 1, True, True): (47, 1, 6, 13),
('ccx', 2, True, True): (119, 16, 6, 84),
('ccx', 3, True, True): (399, 81, 6, 335),
('no_overrides', 1, False, False): (47, 1, 6, 13),
('no_overrides', 2, False, False): (119, 16, 6, 84),
('no_overrides', 3, False, False): (399, 81, 6, 335),
('ccx', 1, False, False): (47, 1, 6, 13),
('ccx', 2, False, False): (119, 16, 6, 84),
('ccx', 3, False, False): (399, 81, 6, 335),
('ccx', 1, False, True): (47, 1, 6, 13),
('ccx', 2, False, True): (119, 16, 6, 84),
('ccx', 3, False, True): (399, 81, 6, 335),
('no_overrides', 1, True, False): (34, 0, 6, 1),
('no_overrides', 2, True, False): (40, 0, 6, 1),
('no_overrides', 3, True, False): (50, 0, 6, 1),
('ccx', 1, True, False): (34, 0, 6, 1),
('ccx', 2, True, False): (40, 0, 6, 1),
('ccx', 3, True, False): (50, 0, 6, 1),
('ccx', 1, True, True): (47, 0, 6, 1),
('ccx', 2, True, True): (40, 0, 6, 1),
('ccx', 3, True, True): (50, 0, 6, 1),
('no_overrides', 1, False, False): (34, 0, 6, 1),
('no_overrides', 2, False, False): (40, 0, 6, 1),
('no_overrides', 3, False, False): (50, 0, 6, 1),
('ccx', 1, False, False): (34, 0, 6, 1),
('ccx', 2, False, False): (40, 0, 6, 1),
('ccx', 3, False, False): (50, 0, 6, 1),
('ccx', 1, False, True): (47, 0, 6, 1),
('ccx', 2, False, True): (40, 0, 6, 1),
('ccx', 3, False, True): (50, 0, 6, 1),
}
......@@ -261,22 +263,22 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True
TEST_DATA = {
('no_overrides', 1, True, False): (47, 1, 4, 9),
('no_overrides', 2, True, False): (119, 16, 19, 54),
('no_overrides', 3, True, False): (399, 81, 84, 215),
('ccx', 1, True, False): (47, 1, 4, 9),
('ccx', 2, True, False): (119, 16, 19, 54),
('ccx', 3, True, False): (399, 81, 84, 215),
('ccx', 1, True, True): (49, 1, 4, 13),
('ccx', 2, True, True): (121, 16, 19, 84),
('ccx', 3, True, True): (401, 81, 84, 335),
('no_overrides', 1, False, False): (47, 1, 4, 9),
('no_overrides', 2, False, False): (119, 16, 19, 54),
('no_overrides', 3, False, False): (399, 81, 84, 215),
('ccx', 1, False, False): (47, 1, 4, 9),
('ccx', 2, False, False): (119, 16, 19, 54),
('ccx', 3, False, False): (399, 81, 84, 215),
('ccx', 1, False, True): (47, 1, 4, 9),
('ccx', 2, False, True): (119, 16, 19, 54),
('ccx', 3, False, True): (399, 81, 84, 215),
('no_overrides', 1, True, False): (34, 0, 4, 1),
('no_overrides', 2, True, False): (40, 0, 19, 1),
('no_overrides', 3, True, False): (50, 0, 84, 1),
('ccx', 1, True, False): (34, 0, 4, 1),
('ccx', 2, True, False): (40, 0, 19, 1),
('ccx', 3, True, False): (50, 0, 84, 1),
('ccx', 1, True, True): (35, 0, 5, 6),
('ccx', 2, True, True): (41, 0, 20, 47),
('ccx', 3, True, True): (51, 0, 85, 202),
('no_overrides', 1, False, False): (34, 0, 4, 1),
('no_overrides', 2, False, False): (40, 0, 19, 1),
('no_overrides', 3, False, False): (50, 0, 84, 1),
('ccx', 1, False, False): (34, 0, 4, 1),
('ccx', 2, False, False): (40, 0, 19, 1),
('ccx', 3, False, False): (50, 0, 84, 1),
('ccx', 1, False, True): (46, 0, 4, 1),
('ccx', 2, False, True): (118, 0, 19, 1),
('ccx', 3, False, True): (398, 0, 84, 1),
}
......@@ -7,7 +7,10 @@ import mock
import pytz
from nose.plugins.attrib import attr
from ccx_keys.locator import CCXLocator
from courseware.courses import get_course_by_id
from courseware.field_overrides import OverrideFieldData
from courseware.testutils import FieldOverrideTestMixin
from django.test.utils import override_settings
from lms.djangoapps.courseware.tests.test_field_overrides import inject_field_overrides
from request_cache.middleware import RequestCache
......@@ -24,9 +27,11 @@ from lms.djangoapps.ccx.tests.utils import flatten, iter_blocks
@attr('shard_1')
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'ccx.overrides.CustomCoursesForEdxOverrideProvider',))
class TestFieldOverrides(SharedModuleStoreTestCase):
@override_settings(
XBLOCK_FIELD_DATA_WRAPPERS=['lms.djangoapps.courseware.field_overrides:OverrideModulestoreFieldData.wrap'],
MODULESTORE_FIELD_OVERRIDE_PROVIDERS=['ccx.overrides.CustomCoursesForEdxOverrideProvider'],
)
class TestFieldOverrides(FieldOverrideTestMixin, SharedModuleStoreTestCase):
"""
Make sure field overrides behave in the expected manner.
"""
......@@ -77,6 +82,9 @@ class TestFieldOverrides(SharedModuleStoreTestCase):
inject_field_overrides(iter_blocks(ccx.course), self.course, AdminFactory.create())
self.ccx_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
self.ccx_course = get_course_by_id(self.ccx_key, depth=None)
def cleanup_provider_classes():
"""
After everything is done, clean up by un-doing the change to the
......@@ -90,7 +98,7 @@ class TestFieldOverrides(SharedModuleStoreTestCase):
Test that overriding start date on a chapter works.
"""
ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
chapter = self.ccx.course.get_children()[0]
chapter = self.ccx_course.get_children()[0]
override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
self.assertEquals(chapter.start, ccx_start)
......@@ -99,7 +107,7 @@ class TestFieldOverrides(SharedModuleStoreTestCase):
Test that for creating new field executed only create query
"""
ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
chapter = self.ccx.course.get_children()[0]
chapter = self.ccx_course.get_children()[0]
# One outer SAVEPOINT/RELEASE SAVEPOINT pair around everything caused by the
# transaction.atomic decorator wrapping override_field_for_ccx.
# One SELECT and one INSERT.
......@@ -114,7 +122,7 @@ class TestFieldOverrides(SharedModuleStoreTestCase):
"""
ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
new_ccx_start = datetime.datetime(2015, 12, 25, 00, 00, tzinfo=pytz.UTC)
chapter = self.ccx.course.get_children()[0]
chapter = self.ccx_course.get_children()[0]
override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
with self.assertNumQueries(3):
override_field_for_ccx(self.ccx, chapter, 'start', new_ccx_start)
......@@ -124,7 +132,7 @@ class TestFieldOverrides(SharedModuleStoreTestCase):
Test that if value of field does not changed no query execute.
"""
ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
chapter = self.ccx.course.get_children()[0]
chapter = self.ccx_course.get_children()[0]
override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
with self.assertNumQueries(2): # 2 savepoints
override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
......@@ -134,7 +142,7 @@ class TestFieldOverrides(SharedModuleStoreTestCase):
Test no extra queries when accessing an overriden field more than once.
"""
ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
chapter = self.ccx.course.get_children()[0]
chapter = self.ccx_course.get_children()[0]
# One outer SAVEPOINT/RELEASE SAVEPOINT pair around everything caused by the
# transaction.atomic decorator wrapping override_field_for_ccx.
# One SELECT and one INSERT.
......@@ -148,7 +156,7 @@ class TestFieldOverrides(SharedModuleStoreTestCase):
Test that sequentials inherit overridden start date from chapter.
"""
ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
chapter = self.ccx.course.get_children()[0]
chapter = self.ccx_course.get_children()[0]
override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
self.assertEquals(chapter.get_children()[0].start, ccx_start)
self.assertEquals(chapter.get_children()[1].start, ccx_start)
......@@ -160,7 +168,7 @@ class TestFieldOverrides(SharedModuleStoreTestCase):
the mooc.
"""
ccx_due = datetime.datetime(2015, 1, 1, 00, 00, tzinfo=pytz.UTC)
chapter = self.ccx.course.get_children()[0]
chapter = self.ccx_course.get_children()[0]
chapter.display_name = 'itsme!'
override_field_for_ccx(self.ccx, chapter, 'due', ccx_due)
vertical = chapter.get_children()[0].get_children()[0]
......
......@@ -15,6 +15,7 @@ from courseware.courses import get_course_by_id
from courseware.tests.factories import StudentModuleFactory
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tabs import get_course_tab_list
from courseware.testutils import FieldOverrideTestMixin
from instructor.access import (
allow_access,
list_with_level,
......@@ -921,10 +922,12 @@ def patched_get_children(self, usage_key_filter=None):
@attr('shard_1')
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'ccx.overrides.CustomCoursesForEdxOverrideProvider',))
@override_settings(
XBLOCK_FIELD_DATA_WRAPPERS=['lms.djangoapps.courseware.field_overrides:OverrideModulestoreFieldData.wrap'],
MODULESTORE_FIELD_OVERRIDE_PROVIDERS=['ccx.overrides.CustomCoursesForEdxOverrideProvider'],
)
@patch('xmodule.x_module.XModuleMixin.get_children', patched_get_children, spec=True)
class TestCCXGrades(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
class TestCCXGrades(FieldOverrideTestMixin, SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Tests for Custom Courses views.
"""
......
......@@ -282,12 +282,6 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke
def prep_course_for_grading(course, request):
"""Set up course module for overrides to function properly"""
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2)
course = get_module_for_descriptor(
request.user, request, course, field_data_cache, course.id, course=course
)
course._field_data_cache = {} # pylint: disable=protected-access
course.set_grading_policy(course.grading_policy)
......
......@@ -17,6 +17,7 @@ from ..field_overrides import (
OverrideFieldData,
OverrideModulestoreFieldData,
)
from ..testutils import FieldOverrideTestMixin
TESTUSER = "testuser"
......@@ -128,15 +129,7 @@ class OverrideFieldDataTests(SharedModuleStoreTestCase):
@override_settings(
MODULESTORE_FIELD_OVERRIDE_PROVIDERS=['courseware.tests.test_field_overrides.TestOverrideProvider']
)
class OverrideModulestoreFieldDataTests(OverrideFieldDataTests):
def setUp(self):
super(OverrideModulestoreFieldDataTests, self).setUp()
OverrideModulestoreFieldData.provider_classes = None
def tearDown(self):
super(OverrideModulestoreFieldDataTests, self).tearDown()
OverrideModulestoreFieldData.provider_classes = None
class OverrideModulestoreFieldDataTests(FieldOverrideTestMixin, OverrideFieldDataTests):
def make_one(self):
return OverrideModulestoreFieldData.wrap(self.course, DictFieldData({
'foo': 'bar',
......
......@@ -339,16 +339,16 @@ class ModuleRenderTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
)
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'ccx.overrides.CustomCoursesForEdxOverrideProvider',
'courseware.student_field_overrides.IndividualStudentOverrideProvider',
))
def test_rebind_different_users_ccx(self):
def test_rebind_different_users(self):
"""
This tests the rebinding a descriptor to a student does not result
in overly nested _field_data when CCX is enabled.
in overly nested _field_data.
"""
request = self.request_factory.get('')
request.user = self.mock_user
course = CourseFactory.create(enable_ccx=True)
course = CourseFactory.create()
descriptor = ItemFactory(category='html', parent=course)
field_data_cache = FieldDataCache(
......
......@@ -8,6 +8,7 @@ import ddt
from mock import patch
from urllib import urlencode
from lms.djangoapps.courseware.field_overrides import OverrideModulestoreFieldData
from lms.djangoapps.courseware.url_helpers import get_redirect_url
from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentFactory
from xmodule.modulestore import ModuleStoreEnum
......@@ -197,3 +198,16 @@ class RenderXBlockTestMixin(object):
self.setup_course()
self.setup_user(admin=False, enroll=True, login=True)
self.verify_response(url_params={'view': 'author_view'}, expected_response_code=400)
class FieldOverrideTestMixin(object):
"""
A Mixin helper class for classes that test Field Overrides.
"""
def setUp(self):
super(FieldOverrideTestMixin, self).setUp()
OverrideModulestoreFieldData.provider_classes = None
def tearDown(self):
super(FieldOverrideTestMixin, self).tearDown()
OverrideModulestoreFieldData.provider_classes = None
......@@ -731,7 +731,7 @@ COURSE_CATALOG_API_URL = ENV_TOKENS.get('COURSE_CATALOG_API_URL', COURSE_CATALOG
##### Custom Courses for EdX #####
if FEATURES.get('CUSTOM_COURSES_EDX'):
INSTALLED_APPS += ('lms.djangoapps.ccx', 'openedx.core.djangoapps.ccxcon')
FIELD_OVERRIDE_PROVIDERS += (
MODULESTORE_FIELD_OVERRIDE_PROVIDERS += (
'lms.djangoapps.ccx.overrides.CustomCoursesForEdxOverrideProvider',
)
CCX_MAX_STUDENTS_ALLOWED = ENV_TOKENS.get('CCX_MAX_STUDENTS_ALLOWED', CCX_MAX_STUDENTS_ALLOWED)
......
......@@ -299,7 +299,7 @@ GRADES_DOWNLOAD_ROUTING_KEY = HIGH_MEM_QUEUE
##### Custom Courses for EdX #####
if FEATURES.get('CUSTOM_COURSES_EDX'):
INSTALLED_APPS += ('lms.djangoapps.ccx', 'openedx.core.djangoapps.ccxcon')
FIELD_OVERRIDE_PROVIDERS += (
MODULESTORE_FIELD_OVERRIDE_PROVIDERS += (
'lms.djangoapps.ccx.overrides.CustomCoursesForEdxOverrideProvider',
)
......
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