field_overrides.py 11.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
"""
This module provides a :class:`~xblock.field_data.FieldData` implementation
which wraps an other `FieldData` object and provides overrides based on the
user.  The use of providers allows for overrides that are arbitrarily
extensible.  One provider is found in `courseware.student_field_overrides`
which allows for fields to be overridden for individual students.  One can
envision other providers being written that allow for fields to be overridden
base on membership of a student in a cohort, or similar.  The use of an
extensible, modular architecture allows for overrides being done in ways not
envisioned by the authors.

Currently, this module is used in the `module_render` module in this same
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.
"""
17
import threading
18 19
from abc import ABCMeta, abstractmethod
from contextlib import contextmanager
20

21
from django.conf import settings
22
from xblock.field_data import FieldData
23

24 25 26
from request_cache.middleware import RequestCache
from xmodule.modulestore.inheritance import InheritanceMixin

27
NOTSET = object()
28 29
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}'
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50


def resolve_dotted(name):
    """
    Given the dotted name for a Python object, performs any necessary imports
    and returns the object.
    """
    names = name.split('.')
    path = names.pop(0)
    target = __import__(path)
    while names:
        segment = names.pop(0)
        path += '.' + segment
        try:
            target = getattr(target, segment)
        except AttributeError:
            __import__(path)
            target = getattr(target, segment)
    return target


51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
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


133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
class OverrideFieldData(FieldData):
    """
    A :class:`~xblock.field_data.FieldData` which wraps another `FieldData`
    object and allows for fields handled by the wrapped `FieldData` to be
    overriden by arbitrary providers.

    Providers are configured by use of the Django setting,
    `FIELD_OVERRIDE_PROVIDERS` which should be a tuple of dotted names of
    :class:`FieldOverrideProvider` concrete implementations.  Note that order
    is important for this setting.  Override providers will tried in the order
    configured in the setting.  The first provider to find an override 'wins'
    for a particular field lookup.
    """
    provider_classes = None

    @classmethod
149
    def wrap(cls, user, course, wrapped):
150 151 152 153 154 155 156 157 158 159 160 161 162
        """
        Will return a :class:`OverrideFieldData` which wraps the field data
        given in `wrapped` for the given `user`, if override providers are
        configred.  If no override providers are configured, using the Django
        setting, `FIELD_OVERRIDE_PROVIDERS`, returns `wrapped`, eliminating
        any performance impact of this feature if no override providers are
        configured.
        """
        if cls.provider_classes is None:
            cls.provider_classes = tuple(
                (resolve_dotted(name) for name in
                 settings.FIELD_OVERRIDE_PROVIDERS))

163 164 165 166 167 168 169
        enabled_providers = cls._providers_for_course(course)
        if enabled_providers:
            # TODO: we might not actually want to return here.  Might be better
            # to check for instance.providers after the instance is built. This
            # would allow for the case where we have registered providers but
            # none are enabled for the provided course
            return cls(user, wrapped, enabled_providers)
170 171 172

        return wrapped

173 174 175 176 177 178 179 180 181 182 183
    @classmethod
    def _providers_for_course(cls, course):
        """
        Return a filtered list of enabled providers based
        on the course passed in. Cache this result per request to avoid
        needing to call the provider filter api hundreds of times.

        Arguments:
            course: The course XBlock
        """
        request_cache = RequestCache.get_request_cache()
184 185 186 187 188
        if course is None:
            cache_key = ENABLED_OVERRIDE_PROVIDERS_KEY.format(course_id='None')
        else:
            cache_key = ENABLED_OVERRIDE_PROVIDERS_KEY.format(course_id=unicode(course.id))
        enabled_providers = request_cache.data.get(cache_key, NOTSET)
189 190 191 192
        if enabled_providers == NOTSET:
            enabled_providers = tuple(
                (provider_class for provider_class in cls.provider_classes if provider_class.enabled_for(course))
            )
193
            request_cache.data[cache_key] = enabled_providers
194 195 196 197

        return enabled_providers

    def __init__(self, user, fallback, providers):
198
        self.fallback = fallback
199
        self.providers = tuple(provider(user) for provider in providers)
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225

    def get_override(self, block, name):
        """
        Checks for an override for the field identified by `name` in `block`.
        Returns the overridden value or `NOTSET` if no override is found.
        """
        if not overrides_disabled():
            for provider in self.providers:
                value = provider.get(block, name, NOTSET)
                if value is not NOTSET:
                    return value
        return NOTSET

    def get(self, block, name):
        value = self.get_override(block, name)
        if value is not NOTSET:
            return value
        return self.fallback.get(block, name)

    def set(self, block, name, value):
        self.fallback.set(block, name, value)

    def delete(self, block, name):
        self.fallback.delete(block, name)

    def has(self, block, name):
226 227 228
        if not self.providers:
            return self.fallback.has(block, name)

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
        has = self.get_override(block, name)
        if has is NOTSET:
            # If this is an inheritable field and an override is set above,
            # then we want to return False here, so the field_data uses the
            # override and not the original value for this block.
            inheritable = InheritanceMixin.fields.keys()
            if name in inheritable:
                for ancestor in _lineage(block):
                    if self.get_override(ancestor, name) is not NOTSET:
                        return False

        return has is not NOTSET or self.fallback.has(block, name)

    def set_many(self, block, update_dict):
        return self.fallback.set_many(block, update_dict)

    def default(self, block, name):
        # The `default` method is overloaded by the field storage system to
        # also handle inheritance.
248
        if self.providers and not overrides_disabled():
249 250 251 252 253 254 255 256 257
            inheritable = InheritanceMixin.fields.keys()
            if name in inheritable:
                for ancestor in _lineage(block):
                    value = self.get_override(ancestor, name)
                    if value is not NOTSET:
                        return value
        return self.fallback.default(block, name)


258 259
class OverrideModulestoreFieldData(OverrideFieldData):
    """Apply field data overrides at the modulestore level. No student context required."""
260
    provider_classes = None
261

262 263 264 265 266 267
    @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.
268

269 270 271 272 273 274 275 276
        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
            ]
277

278 279 280
        enabled_providers = cls._providers_for_block(block)
        if enabled_providers:
            return cls(field_data, enabled_providers)
281

282
        return field_data
283

284 285
    @classmethod
    def _providers_for_block(cls, block):
286
        """
287 288 289
        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.
290

291 292
        Arguments:
            block: An XBlock
293
        """
294 295
        course_id = unicode(block.location.course_key)
        cache_key = ENABLED_MODULESTORE_OVERRIDE_PROVIDERS_KEY.format(course_id=course_id)
296

297 298
        request_cache = RequestCache.get_request_cache()
        enabled_providers = request_cache.data.get(cache_key)
299

300 301 302 303 304
        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
305

306
        return enabled_providers
307

308 309
    def __init__(self, fallback, providers):
        super(OverrideModulestoreFieldData, self).__init__(None, fallback, providers)