Override field data within the XBlock runtime

Resolves an issue preventing students in self-paced courses from seeing all available discussion modules. ECOM-3733.
This commit is contained in:
Renzo Lucioni
2016-03-10 17:33:19 -05:00
parent 2536c58920
commit cd9986b662
13 changed files with 348 additions and 134 deletions

View File

@@ -283,6 +283,17 @@ else:
DATABASES = AUTH_TOKENS['DATABASES']
MODULESTORE = convert_module_store_setting_if_needed(AUTH_TOKENS.get('MODULESTORE', MODULESTORE))
MODULESTORE_FIELD_OVERRIDE_PROVIDERS = ENV_TOKENS.get(
'MODULESTORE_FIELD_OVERRIDE_PROVIDERS',
MODULESTORE_FIELD_OVERRIDE_PROVIDERS
)
XBLOCK_FIELD_DATA_WRAPPERS = ENV_TOKENS.get(
'XBLOCK_FIELD_DATA_WRAPPERS',
XBLOCK_FIELD_DATA_WRAPPERS
)
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
DOC_STORE_CONFIG = AUTH_TOKENS['DOC_STORE_CONFIG']
# Datadog for events!

View File

@@ -383,6 +383,9 @@ XBLOCK_MIXINS = (
XBLOCK_SELECT_FUNCTION = prefer_xmodules
# Paths to wrapper methods which should be applied to every XBlock's FieldData.
XBLOCK_FIELD_DATA_WRAPPERS = ()
############################ Modulestore Configuration ################################
MODULESTORE_BRANCH = 'draft-preferred'
@@ -417,6 +420,10 @@ MODULESTORE = {
}
}
# Modulestore-level field override providers. These field override providers don't
# require student context.
MODULESTORE_FIELD_OVERRIDE_PROVIDERS = ()
#################### Python sandbox ############################################
CODE_JAIL = {

View File

@@ -1160,7 +1160,7 @@ class ModuleStoreReadBase(BulkOperationsMixin, ModuleStoreRead):
contentstore=None,
doc_store_config=None, # ignore if passed up
metadata_inheritance_cache_subsystem=None, request_cache=None,
xblock_mixins=(), xblock_select=None, disabled_xblock_types=(), # pylint: disable=bad-continuation
xblock_mixins=(), xblock_select=None, xblock_field_data_wrappers=(), disabled_xblock_types=(), # pylint: disable=bad-continuation
# temporary parms to enable backward compatibility. remove once all envs migrated
db=None, collection=None, host=None, port=None, tz_aware=True, user=None, password=None,
# allow lower level init args to pass harmlessly
@@ -1177,6 +1177,7 @@ class ModuleStoreReadBase(BulkOperationsMixin, ModuleStoreRead):
self.request_cache = request_cache
self.xblock_mixins = xblock_mixins
self.xblock_select = xblock_select
self.xblock_field_data_wrappers = xblock_field_data_wrappers
self.disabled_xblock_types = disabled_xblock_types
self.contentstore = contentstore

View File

@@ -120,11 +120,25 @@ def load_function(path):
"""
Load a function by name.
path is a string of the form "path.to.module.function"
returns the imported python object `function` from `path.to.module`
Arguments:
path: String of the form 'path.to.module.function'. Strings of the form
'path.to.module:Class.function' are also valid.
Returns:
The imported object 'function'.
"""
module_path, _, name = path.rpartition('.')
return getattr(import_module(module_path), name)
if ':' in path:
module_path, _, method_path = path.rpartition(':')
module = import_module(module_path)
class_name, method_name = method_path.split('.')
_class = getattr(module, class_name)
function = getattr(_class, method_name)
else:
module_path, _, name = path.rpartition('.')
function = getattr(import_module(module_path), name)
return function
def create_modulestore_instance(
@@ -179,12 +193,15 @@ def create_modulestore_instance(
else:
disabled_xblock_types = ()
xblock_field_data_wrappers = [load_function(path) for path in settings.XBLOCK_FIELD_DATA_WRAPPERS]
return class_(
contentstore=content_store,
metadata_inheritance_cache_subsystem=metadata_inheritance_cache,
request_cache=request_cache,
xblock_mixins=getattr(settings, 'XBLOCK_MIXINS', ()),
xblock_select=getattr(settings, 'XBLOCK_SELECT_FUNCTION', None),
xblock_field_data_wrappers=xblock_field_data_wrappers,
disabled_xblock_types=disabled_xblock_types,
doc_store_config=doc_store_config,
i18n_service=i18n_service or ModuleI18nService(),

View File

@@ -218,6 +218,17 @@ class InheritanceMixin(XBlockMixin):
default=False
)
self_paced = Boolean(
display_name=_('Self Paced'),
help=_(
'Set this to "true" to mark this course as self-paced. Self-paced courses do not have '
'due dates for assignments, and students can progress through the course at any rate before '
'the course ends.'
),
default=False,
scope=Scope.settings
)
def compute_inherited_metadata(descriptor):
"""Given a descriptor, traverse all of its descendants and do metadata

View File

@@ -12,27 +12,24 @@ structure:
}
"""
import pymongo
import sys
import logging
import copy
from datetime import datetime
from importlib import import_module
import logging
import pymongo
import re
import sys
from uuid import uuid4
from bson.son import SON
from datetime import datetime
from contracts import contract, new_contract
from fs.osfs import OSFS
from mongodb_proxy import autoretry_read
from opaque_keys.edx.keys import UsageKey, CourseKey, AssetKey
from opaque_keys.edx.locations import Location, BlockUsageLocator, SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
from path import Path as path
from pytz import UTC
from contracts import contract, new_contract
from importlib import import_module
from opaque_keys.edx.keys import UsageKey, CourseKey, AssetKey
from opaque_keys.edx.locations import Location, BlockUsageLocator
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
from xblock.core import XBlock
from xblock.exceptions import InvalidScopeError
from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict
@@ -54,6 +51,7 @@ from xmodule.modulestore.xml import CourseLocationManager
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
from xmodule.services import SettingsService
log = logging.getLogger(__name__)
new_contract('CourseKey', CourseKey)
@@ -318,6 +316,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
).replace(tzinfo=UTC)
module._edit_info['published_by'] = raw_metadata.get('published_by')
for wrapper in self.modulestore.xblock_field_data_wrappers:
module._field_data = wrapper(module, module._field_data) # pylint: disable=protected-access
# decache any computed pending field settings
module.save()
return module

View File

@@ -1,5 +1,6 @@
import sys
import logging
from contracts import contract, new_contract
from fs.osfs import OSFS
from lazy import lazy
@@ -7,6 +8,7 @@ from xblock.runtime import KvsFieldData, KeyValueStore
from xblock.fields import ScopeIds
from xblock.core import XBlock
from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, LibraryLocator, DefinitionLocator
from xmodule.library_tools import LibraryToolsService
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor
@@ -263,6 +265,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
module.update_version = edit_info.update_version
module.source_version = edit_info.source_version
module.definition_locator = DefinitionLocator(block_key.type, definition_id)
for wrapper in self.modulestore.xblock_field_data_wrappers:
module._field_data = wrapper(module, module._field_data) # pylint: disable=protected-access
# decache any pending field settings
module.save()

View File

@@ -14,17 +14,20 @@ package and is used to wrap the `authored_data` when constructing an
`LmsFieldData`. This means overrides will be in effect for all scopes covered
by `authored_data`, e.g. course content and settings stored in Mongo.
"""
import threading
from abc import ABCMeta, abstractmethod
from contextlib import contextmanager
import threading
from django.conf import settings
from request_cache.middleware import RequestCache
from xblock.field_data import FieldData
from request_cache.middleware import RequestCache
from xmodule.modulestore.inheritance import InheritanceMixin
NOTSET = object()
ENABLED_OVERRIDE_PROVIDERS_KEY = "courseware.field_overrides.enabled_providers.{course_id}"
ENABLED_OVERRIDE_PROVIDERS_KEY = u'courseware.field_overrides.enabled_providers.{course_id}'
ENABLED_MODULESTORE_OVERRIDE_PROVIDERS_KEY = u'courseware.modulestore_field_overrides.enabled_providers.{course_id}'
def resolve_dotted(name):
@@ -46,6 +49,88 @@ def resolve_dotted(name):
return target
def _lineage(block):
"""
Returns an iterator over all ancestors of the given block, starting with
its immediate parent and ending at the root of the block tree.
"""
parent = block.get_parent()
while parent:
yield parent
parent = parent.get_parent()
class _OverridesDisabled(threading.local):
"""
A thread local used to manage state of overrides being disabled or not.
"""
disabled = ()
_OVERRIDES_DISABLED = _OverridesDisabled()
@contextmanager
def disable_overrides():
"""
A context manager which disables field overrides inside the context of a
`with` statement, allowing code to get at the `original` value of a field.
"""
prev = _OVERRIDES_DISABLED.disabled
_OVERRIDES_DISABLED.disabled += (True,)
yield
_OVERRIDES_DISABLED.disabled = prev
def overrides_disabled():
"""
Checks to see whether overrides are disabled in the current context.
Returns a boolean value. See `disable_overrides`.
"""
return bool(_OVERRIDES_DISABLED.disabled)
class FieldOverrideProvider(object):
"""
Abstract class which defines the interface that a `FieldOverrideProvider`
must provide. In general, providers should derive from this class, but
it's not strictly necessary as long as they correctly implement this
interface.
A `FieldOverrideProvider` implementation is only responsible for looking up
field overrides. To set overrides, there will be a domain specific API for
the concrete override implementation being used.
"""
__metaclass__ = ABCMeta
def __init__(self, user):
self.user = user
@abstractmethod
def get(self, block, name, default): # pragma no cover
"""
Look for an override value for the field named `name` in `block`.
Returns the overridden value or `default` if no override is found.
"""
raise NotImplementedError
@abstractmethod
def enabled_for(self, course): # pragma no cover
"""
Return True if this provider should be enabled for a given course,
and False otherwise.
Concrete implementations are responsible for implementing this method.
Arguments:
course (CourseModule or None)
Returns:
bool
"""
return False
class OverrideFieldData(FieldData):
"""
A :class:`~xblock.field_data.FieldData` which wraps another `FieldData`
@@ -171,83 +256,54 @@ class OverrideFieldData(FieldData):
return self.fallback.default(block, name)
class _OverridesDisabled(threading.local):
"""
A thread local used to manage state of overrides being disabled or not.
"""
disabled = ()
class OverrideModulestoreFieldData(OverrideFieldData):
"""Apply field data overrides at the modulestore level. No student context required."""
_OVERRIDES_DISABLED = _OverridesDisabled()
@contextmanager
def disable_overrides():
"""
A context manager which disables field overrides inside the context of a
`with` statement, allowing code to get at the `original` value of a field.
"""
prev = _OVERRIDES_DISABLED.disabled
_OVERRIDES_DISABLED.disabled += (True,)
yield
_OVERRIDES_DISABLED.disabled = prev
def overrides_disabled():
"""
Checks to see whether overrides are disabled in the current context.
Returns a boolean value. See `disable_overrides`.
"""
return bool(_OVERRIDES_DISABLED.disabled)
class FieldOverrideProvider(object):
"""
Abstract class which defines the interface that a `FieldOverrideProvider`
must provide. In general, providers should derive from this class, but
it's not strictly necessary as long as they correctly implement this
interface.
A `FieldOverrideProvider` implementation is only responsible for looking up
field overrides. To set overrides, there will be a domain specific API for
the concrete override implementation being used.
"""
__metaclass__ = ABCMeta
def __init__(self, user):
self.user = user
@abstractmethod
def get(self, block, name, default): # pragma no cover
@classmethod
def wrap(cls, block, field_data): # pylint: disable=arguments-differ
"""
Look for an override value for the field named `name` in `block`.
Returns the overridden value or `default` if no override is found.
"""
raise NotImplementedError
@abstractmethod
def enabled_for(self, course): # pragma no cover
"""
Return True if this provider should be enabled for a given course,
and False otherwise.
Concrete implementations are responsible for implementing this method.
Returns an instance of FieldData wrapped by FieldOverrideProviders which
extend read-only functionality. If no MODULESTORE_FIELD_OVERRIDE_PROVIDERS
are configured, an unwrapped FieldData instance is returned.
Arguments:
course (CourseModule or None)
Returns:
bool
block: An XBlock
field_data: An instance of FieldData to be wrapped
"""
return False
if cls.provider_classes is None:
cls.provider_classes = [
resolve_dotted(name) for name in settings.MODULESTORE_FIELD_OVERRIDE_PROVIDERS
]
enabled_providers = cls._providers_for_block(block)
if enabled_providers:
return cls(field_data, enabled_providers)
def _lineage(block):
"""
Returns an iterator over all ancestors of the given block, starting with
its immediate parent and ending at the root of the block tree.
"""
parent = block.get_parent()
while parent:
yield parent
parent = parent.get_parent()
return field_data
@classmethod
def _providers_for_block(cls, block):
"""
Computes a list of enabled providers based on the given XBlock.
The result is cached per request to avoid the overhead incurred
by filtering override providers hundreds of times.
Arguments:
block: An XBlock
"""
course_id = unicode(block.location.course_key)
cache_key = ENABLED_MODULESTORE_OVERRIDE_PROVIDERS_KEY.format(course_id=course_id)
request_cache = RequestCache.get_request_cache()
enabled_providers = request_cache.data.get(cache_key)
if enabled_providers is None:
enabled_providers = [
provider_class for provider_class in cls.provider_classes if provider_class.enabled_for(block)
]
request_cache.data[cache_key] = enabled_providers
return enabled_providers
def __init__(self, fallback, providers):
super(OverrideModulestoreFieldData, self).__init__(None, fallback, providers)

View File

@@ -20,9 +20,10 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider):
# Remove release dates for course content
if name == 'start' and block.category != 'course':
return None
return default
@classmethod
def enabled_for(cls, course):
def enabled_for(cls, block):
"""This provider is enabled for self-paced courses only."""
return course is not None and course.self_paced and SelfPacedConfiguration.current().enabled
return block is not None and block.self_paced and SelfPacedConfiguration.current().enabled

View File

@@ -1,6 +1,7 @@
"""
Tests for `field_overrides` module.
"""
# pylint: disable=missing-docstring
import unittest
from nose.plugins.attrib import attr
@@ -10,16 +11,39 @@ from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from ..field_overrides import (
resolve_dotted,
disable_overrides,
FieldOverrideProvider,
OverrideFieldData,
resolve_dotted,
OverrideModulestoreFieldData,
)
TESTUSER = "testuser"
class TestOverrideProvider(FieldOverrideProvider):
"""
A concrete implementation of `FieldOverrideProvider` for testing.
"""
def get(self, block, name, default):
if self.user:
assert self.user is TESTUSER
assert block == 'block'
if name == 'foo':
return 'fu'
elif name == 'oh':
return 'man'
return default
@classmethod
def enabled_for(cls, course):
return True
@attr('shard_1')
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'courseware.tests.test_field_overrides.TestOverrideProvider',))
@@ -100,6 +124,31 @@ class OverrideFieldDataTests(SharedModuleStoreTestCase):
self.assertIsInstance(data, DictFieldData)
@attr('shard_1')
@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
def make_one(self):
return OverrideModulestoreFieldData.wrap(self.course, DictFieldData({
'foo': 'bar',
'bees': 'knees',
}))
@override_settings(MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[])
def test_no_overrides_configured(self):
data = self.make_one()
self.assertIsInstance(data, DictFieldData)
@attr('shard_1')
class ResolveDottedTests(unittest.TestCase):
"""
@@ -121,24 +170,6 @@ class ResolveDottedTests(unittest.TestCase):
)
class TestOverrideProvider(FieldOverrideProvider):
"""
A concrete implementation of `FieldOverrideProvider` for testing.
"""
def get(self, block, name, default):
assert self.user is TESTUSER
assert block == 'block'
if name == 'foo':
return 'fu'
if name == 'oh':
return 'man'
return default
@classmethod
def enabled_for(cls, course):
return True
def inject_field_overrides(blocks, course, user):
"""
Apparently the test harness doesn't use LmsFieldStorage, and I'm

View File

@@ -1,7 +1,5 @@
"""
Tests for self-paced course due date overrides.
"""
"""Tests for self-paced course due date overrides."""
# pylint: disable=missing-docstring
import datetime
import pytz
@@ -11,17 +9,17 @@ from mock import patch
from courseware.tests.factories import BetaTesterFactory
from courseware.access import has_access
from lms.djangoapps.ccx.tests.test_overrides import inject_field_overrides
from lms.djangoapps.courseware.field_overrides import OverrideFieldData
from lms.djangoapps.django_comment_client.utils import get_accessible_discussion_modules
from lms.djangoapps.courseware.field_overrides import OverrideFieldData, OverrideModulestoreFieldData
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@override_settings(
FIELD_OVERRIDE_PROVIDERS=('courseware.self_paced_overrides.SelfPacedDateOverrideProvider',)
XBLOCK_FIELD_DATA_WRAPPERS=['lms.djangoapps.courseware.field_overrides:OverrideModulestoreFieldData.wrap'],
MODULESTORE_FIELD_OVERRIDE_PROVIDERS=['courseware.self_paced_overrides.SelfPacedDateOverrideProvider'],
)
class SelfPacedDateOverrideTest(ModuleStoreTestCase):
"""
@@ -29,14 +27,19 @@ class SelfPacedDateOverrideTest(ModuleStoreTestCase):
"""
def setUp(self):
SelfPacedConfiguration(enabled=True).save()
super(SelfPacedDateOverrideTest, self).setUp()
self.due_date = datetime.datetime(2015, 5, 26, 8, 30, 00).replace(tzinfo=tzutc())
SelfPacedConfiguration(enabled=True).save()
self.non_staff_user, __ = self.create_non_staff_user()
self.now = datetime.datetime.now(pytz.UTC).replace(microsecond=0)
self.future = self.now + datetime.timedelta(days=30)
def tearDown(self):
super(SelfPacedDateOverrideTest, self).tearDown()
OverrideFieldData.provider_classes = None
OverrideModulestoreFieldData.provider_classes = None
def setup_course(self, **course_kwargs):
"""Set up a course with provided course attributes.
@@ -45,22 +48,39 @@ class SelfPacedDateOverrideTest(ModuleStoreTestCase):
overrides are correctly applied for both blocks.
"""
course = CourseFactory.create(**course_kwargs)
section = ItemFactory.create(parent=course, due=self.due_date)
section = ItemFactory.create(parent=course, due=self.now)
inject_field_overrides((course, section), course, self.user)
return (course, section)
def test_instructor_paced(self):
__, ip_section = self.setup_course(display_name="Instructor Paced Course", self_paced=False)
self.assertEqual(self.due_date, ip_section.due)
def create_discussion_modules(self, parent):
# Create a released discussion module
ItemFactory.create(
parent=parent,
category='discussion',
display_name='released',
start=self.now,
)
def test_self_paced(self):
# Create a scheduled discussion module
ItemFactory.create(
parent=parent,
category='discussion',
display_name='scheduled',
start=self.future,
)
def test_instructor_paced_due_date(self):
__, ip_section = self.setup_course(display_name="Instructor Paced Course", self_paced=False)
self.assertEqual(ip_section.due, self.now)
def test_self_paced_due_date(self):
__, sp_section = self.setup_course(display_name="Self-Paced Course", self_paced=True)
self.assertIsNone(sp_section.due)
def test_self_paced_disabled(self):
def test_self_paced_disabled_due_date(self):
SelfPacedConfiguration(enabled=False).save()
__, sp_section = self.setup_course(display_name="Self-Paced Course", self_paced=True)
self.assertEqual(self.due_date, sp_section.due)
self.assertEqual(sp_section.due, self.now)
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_course_access_to_beta_users(self):
@@ -89,3 +109,34 @@ class SelfPacedDateOverrideTest(ModuleStoreTestCase):
# Verify beta tester can access the course as well as the course sections
self.assertTrue(has_access(beta_tester, 'load', self_paced_course))
self.assertTrue(has_access(beta_tester, 'load', self_paced_section, self_paced_course.id))
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_instructor_paced_discussion_module_visibility(self):
"""
Verify that discussion modules scheduled for release in the future are
not visible to students in an instructor-paced course.
"""
course, section = self.setup_course(start=self.now, self_paced=False)
self.create_discussion_modules(section)
# Only the released module should be visible when the course is instructor-paced.
modules = get_accessible_discussion_modules(course, self.non_staff_user)
self.assertTrue(
all(module.display_name == 'released' for module in modules)
)
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_self_paced_discussion_module_visibility(self):
"""
Regression test. Verify that discussion modules scheduled for release
in the future are visible to students in a self-paced course.
"""
course, section = self.setup_course(start=self.now, self_paced=True)
self.create_discussion_modules(section)
# The scheduled module should be visible when the course is self-paced.
modules = get_accessible_discussion_modules(course, self.non_staff_user)
self.assertEqual(len(modules), 2)
self.assertTrue(
any(module.display_name == 'scheduled' for module in modules)
)

View File

@@ -389,7 +389,7 @@ if FEATURES.get('ENABLE_CORS_HEADERS') or FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN = ENV_TOKENS.get('CROSS_DOMAIN_CSRF_COOKIE_DOMAIN')
# Field overrides. To use the IDDE feature, add
# Field overrides. To use the IDDE feature, add
# 'courseware.student_field_overrides.IndividualStudentOverrideProvider'.
FIELD_OVERRIDE_PROVIDERS = tuple(ENV_TOKENS.get('FIELD_OVERRIDE_PROVIDERS', []))
@@ -406,6 +406,16 @@ if 'DJFS' in AUTH_TOKENS and AUTH_TOKENS['DJFS'] is not None:
############### Module Store Items ##########
HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = ENV_TOKENS.get('HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS', {})
MODULESTORE_FIELD_OVERRIDE_PROVIDERS = ENV_TOKENS.get(
'MODULESTORE_FIELD_OVERRIDE_PROVIDERS',
MODULESTORE_FIELD_OVERRIDE_PROVIDERS
)
XBLOCK_FIELD_DATA_WRAPPERS = ENV_TOKENS.get(
'XBLOCK_FIELD_DATA_WRAPPERS',
XBLOCK_FIELD_DATA_WRAPPERS
)
############### Mixed Related(Secure/Not-Secure) Items ##########
LMS_SEGMENT_KEY = AUTH_TOKENS.get('SEGMENT_KEY')
@@ -693,7 +703,11 @@ if FEATURES.get('INDIVIDUAL_DUE_DATES'):
)
##### Self-Paced Course Due Dates #####
FIELD_OVERRIDE_PROVIDERS += (
XBLOCK_FIELD_DATA_WRAPPERS += (
'lms.djangoapps.courseware.field_overrides:OverrideModulestoreFieldData.wrap',
)
MODULESTORE_FIELD_OVERRIDE_PROVIDERS += (
'courseware.self_paced_overrides.SelfPacedDateOverrideProvider',
)

View File

@@ -694,6 +694,9 @@ XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin, XModuleMixin, EditInfoMixin)
# Allow any XBlock in the LMS
XBLOCK_SELECT_FUNCTION = prefer_xmodules
# Paths to wrapper methods which should be applied to every XBlock's FieldData.
XBLOCK_FIELD_DATA_WRAPPERS = ()
############# ModuleStore Configuration ##########
MODULESTORE_BRANCH = 'published-only'
@@ -2643,6 +2646,10 @@ CHECKPOINT_PATTERN = r'(?P<checkpoint_name>[^/]+)'
# this setting.
FIELD_OVERRIDE_PROVIDERS = ()
# Modulestore-level field override providers. These field override providers don't
# require student context.
MODULESTORE_FIELD_OVERRIDE_PROVIDERS = ()
# PROFILE IMAGE CONFIG
# WARNING: Certain django storage backends do not support atomic
# file overwrites (including the default, OverwriteStorage) - instead