Commit a5b10ca0 by Renzo Lucioni

Merge pull request #11805 from edx/renzo/self-paced-modulestore-wrapper

Override field data within the XBlock runtime
parents 7eb079df cd9986b6
......@@ -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!
......
......@@ -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 = {
......
......@@ -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
......
......@@ -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(),
......
......@@ -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
......
......@@ -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 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.locations import Location, BlockUsageLocator, SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
from path import Path as path
from pytz import UTC
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
......
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()
......
......@@ -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 = ()
_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 OverrideModulestoreFieldData(OverrideFieldData):
"""Apply field data overrides at the modulestore level. No student context required."""
@classmethod
def wrap(cls, block, field_data): # pylint: disable=arguments-differ
"""
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.
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.
Arguments:
block: An XBlock
field_data: An instance of FieldData to be wrapped
"""
if cls.provider_classes is None:
cls.provider_classes = [
resolve_dotted(name) for name in settings.MODULESTORE_FIELD_OVERRIDE_PROVIDERS
]
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
enabled_providers = cls._providers_for_block(block)
if enabled_providers:
return cls(field_data, enabled_providers)
def __init__(self, user):
self.user = user
return field_data
@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.
@classmethod
def _providers_for_block(cls, block):
"""
raise NotImplementedError
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.
@abstractmethod
def enabled_for(self, course): # pragma no cover
Arguments:
block: An XBlock
"""
Return True if this provider should be enabled for a given course,
and False otherwise.
Concrete implementations are responsible for implementing this method.
course_id = unicode(block.location.course_key)
cache_key = ENABLED_MODULESTORE_OVERRIDE_PROVIDERS_KEY.format(course_id=course_id)
Arguments:
course (CourseModule or None)
request_cache = RequestCache.get_request_cache()
enabled_providers = request_cache.data.get(cache_key)
Returns:
bool
"""
return False
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 _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()
def __init__(self, fallback, providers):
super(OverrideModulestoreFieldData, self).__init__(None, fallback, providers)
......@@ -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
"""
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',))
......@@ -101,6 +125,31 @@ class OverrideFieldDataTests(SharedModuleStoreTestCase):
@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):
"""
Tests for `resolve_dotted`.
......@@ -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
......
"""
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):
def create_discussion_modules(self, parent):
# Create a released discussion module
ItemFactory.create(
parent=parent,
category='discussion',
display_name='released',
start=self.now,
)
# 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(self.due_date, ip_section.due)
self.assertEqual(ip_section.due, self.now)
def test_self_paced(self):
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)
)
......@@ -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',
)
......
......@@ -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'
......@@ -2644,6 +2647,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
......
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