add doc strings; fix pep8 warning address some minor issues brought up by the code review process
205 lines
7.1 KiB
Python
205 lines
7.1 KiB
Python
"""
|
|
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.
|
|
"""
|
|
import threading
|
|
|
|
from abc import ABCMeta, abstractmethod
|
|
from contextlib import contextmanager
|
|
from django.conf import settings
|
|
from xblock.field_data import FieldData
|
|
from xmodule.modulestore.inheritance import InheritanceMixin
|
|
|
|
|
|
NOTSET = object()
|
|
|
|
|
|
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
|
|
|
|
|
|
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
|
|
def wrap(cls, user, wrapped):
|
|
"""
|
|
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))
|
|
|
|
if cls.provider_classes:
|
|
return cls(user, wrapped)
|
|
|
|
return wrapped
|
|
|
|
def __init__(self, user, fallback):
|
|
self.fallback = fallback
|
|
self.providers = tuple((cls(user) for cls in self.provider_classes))
|
|
|
|
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):
|
|
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.
|
|
if not overrides_disabled():
|
|
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)
|
|
|
|
|
|
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
|
|
|
|
|
|
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()
|